This is a step-by-step tutorial for creating a simple web server for fetching facts from MentalFloss API, and list them in a simple HTML template
Please help us improve ourselves for the next time with a quick feedback https://forms.gle/fNxcqSBdyZepoyVT6.
all relevant slides are available at [go slides]: http://present.minutemediaservices.com/gopresent
The entrypoint described below is intended for setting up your environment and placing you in a ready-to-go folder you can start your project from.
Each exercise continues the previous one by adding or changing functionality.
If you are encountering issues you can use the steps defined in each exercise for more detailed wolkthrough.
Furthermore, you can find the implementation for each exercise under folder coolfacts/exerciseN/...
Also, you can get a better perspective by running each exercise by
cd /path/to/go-workshop/coolfacts
go run ./exerciseN
Hope you will have fun and good luck :)
- Entrypoint - Hello World
- Exercise 1 - ping
- Exercise 2 - list facts as JSON
- Exercise 3 - create a new fact
- Exercise 4 - list the facts as HTML
- Exercise 5 - use MentalFloss API
- Exercise 6 - separate to packages
- Exercise 7 - add ticker for updating the facts
- Exercise 8 - refactor
- Idiometic Go links
By the way, all the gophers images are taken from the wonderfull https://github.com/egonelbre/gophers
Build and run
For install go and editor, see here
Clone the project in your favourite terminal,
git clone https://github.com/FTBpro/go-workshop.git
cd into the entrypoint
folder and run the entrypoint project:
cd go-workshop/coolfacts/entrypoint/
go run .
For more details on build and run, you can checkout this readme
If everything was successfull you should see a lovely welcome msg in your terminal 😸
Be sure you know where the code which prints this line is coming from. (hint: you can find it in entrypoint/main.go
)
For more details on build and run, you can checkout this readme
For all further exercises you can continue to write the code in this folder, (in main.go
and later in other files)
At any time if you are having any issues, you can use the reference for the exercise implementation under /exerciseN/...
First use of http
package with a simple server
When navigating to http://localhost:9002/ping
the browser should show PONG
string
From main function, you will need to register to /ping
pattern.
You can use http.HandleFunc
for doing that in a simple way.
For example:
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
// place your code here
}
For printing into
http.ResponseWriter
you can usefmt.Fprintf
, and in case of an error you can usehttp.Error
function
Next you will need to have a server listening on port :9002 to get the ping.
We will use the default server in the http package using http.ListenAndServe
.
For example:
http.ListenAndServe(":9002", nil)
Create /facts
endpoint for listing facts in a JSON format by using a repository.
http://localhost:9002/facts
will show a JSON of all facts in repository, for now it will be hard coded facts.
Create a struct named fact (type Fact struct {...}
)
The fact
struct should have 2 string fields : Image
, Description
Create a struct named repository (or repo)
The repository can use in memory cache for storing the facts, it can be done by one field facts
of type []fact
(a slice of facts).
Add repository functionality:
func (r *repository) getAll() []fact {…}
- The method should return all facts in the
repository.facts
field
- The method should return all facts in the
func (r *repository) add(f fact) {…}
- The method should add the given fact f to the repository
For adding to a slice you can userepository.facts = append(repository.facts, f)
- The method should add the given fact f to the repository
Init the repository from main
with some static data.
Like in the ping from previous exercise, use an anonymous function as an argument to http.HandleFunc
function.
In this function you will:
- Get all the facts from the repository
- Write the facts to the
ResponseWriter
in a JSON format
Use
json.Marshal
to format the struct as json and to write to theResponseWriter
Create a new fact by a POST request.
Create a new fact and add it to the repository by issuing a POST /facts
request with the next payloiad:
{
"image": "image/url",
"description": "image description"
}
For issuing a POST request you can use the next command from terminal while your server is running:
curl --header "Content-Type: application/json" --request POST --data '{"image":"<insertImageURL>", "description": "<insertDescription>"}' http://localhost:9002/facts
In the handler from the previous exercise check for the request method (GET/POST) and add the logic of this exercise under POST section
First, read the request body into a byte stream using ioutil.ReadAll
:
b, err := ioutil.ReadAll(r.Body)
Next, we need to parse this data into some sort of a "request model". We which use a struct, which should be a representation of the request payload.
In this exercise, the expected request payload is:
{
"image": "image/url",
"description": "image description"
}
So our request model struct can be something like this:
var req struct {
Image string `json:"image"`
Description string `json:"description"`
}
Now we need to parse the data into this struct, for this we can use json.Unmarshal
:
err = json.Unmarshal(b, &req)
Finally, after we have this struct filled, create a new fact from it, and add it to the repository.
For adding it to the repository you should use the factRepo.Add
from exercise 2.
- Using HTML template
- Replace the JSON representation from exercise 2 with an HTML
GET /facts
will list the facts in HTML
http://localhost:9002/facts
will show a all facts in repository in HTML.
We will use html/template
package.
For a very basic use, you can declare this variable outside of main
var newsTemplate = `<!DOCTYPE html>
<html>
<head><style>/* copy coolfacts/styles.css for some color 🎨*/</style></head>
<body>
<h1>Facts List</h1>
<div>
{{ range . }}
<article>
<h3>{{.Description}}</h3>
<img src="{{.Image}}" width="100%" />
</article>
{{ end }}
<div>
</body>
</html>`
This is called a 'package defined variable' (global variable), We can use it from anywhere in the package it defined in
This step will replace the JSON you added in exercise 2, so you will need to replace the code in the section under GET method in the handler for /facts
.
Using html/template
, create a new template from the newsTemplate
defined earlier:
tmpl, err := template.New("facts").Parse(newsTemplate)
Next, all you need to do is just to execute the template with the facts, and the http.ResponseWriter:
facts := factsRepo.getAll()
err = tmpl.Execute(w, facts)
Use MetnalFloss API for fetching the facts and initialize the repository, instead of the static data
GET /facts
should show facts from MentalFloss.
This will be done by sending request to the external provider (MentalFloss) to fetch facts and saving them in the repository You can use this API for fetching the data:
http://mentalfloss.com/api/facts
Open a new file names mentalfloss.go
, still in the same folder (package main).
In that file, create a struct names mentalfloss
, for now it will be an empty struct:
type mentalfloss struct{}
This struct will used as the provider for fetching the facts.
Attach a method for fetching the facts to mentalfloss
:
func (mf mentalfloss) Facts() ([]fact, error) {…}
For fetching the facts, call http://mentalfloss.com/api/facts
using http.Get
:
resp, err := http.Get("http://mentalfloss.com/api/facts")
if err != nil {
...
return nil, err
}
defer resp.Body.Close()
A
defer
statement defers the execution of a function until the surrounding function returns. This is how we make sure that we close the response body before we exit the function.
This API returns an array of JSON representation of MentalFloss facts.
The fields which interet us in the API are:
[
{
"fact": "fact text",
"primaryImage": "image/url"
},
{
"fact": "other fact text",
"primaryImage": "image/url"
}
]
You will need to parse the response body into a custom struct like in exercise 3 using json.Unmarshal
Here, a request struct, matching the payload of the response can be:
var items []struct {
FactText string `json:"fact"`
PrimaryImage string `json:"primaryImage"`
}
Like in exercise 3, parse the response body into a custom struct using json.Unmarshal
.
In main
function, replace the hard coded facts with facts from mentalfloss
.
Separate structs into packages.
- Move the fact entity and repository into
fact
package - Move mentalfloss functionality into
mentalfloss
package - Move the http handlers into a into
http
package (which we will create in our project, notnet/http
...)
Create a new folder named fact
and a new file named fact.go
. This file will contain the fact entity and the repository.
In the top of the file add
package fact
Move the fact
and the repository
structs into that file.
Make sure that the struct
Fact
is exported (capitalized)
Create a new folder mentalfloss
and a new file named mentalfloss.go
and move mentalfloss struct and methods into that folder
Set the package name in that file as package mentalfloss
You will encounter compile error since now the
fact
is in another package. You will need to import your fact package And replacefact
withfact.Fact
.
(for example in exercise 6import "github.com/FTBpro/go-workshop/coolfacts/exercise6/fact"
)
The goal is to separate our application http.HandlerFunc
logic outside of main.
Create a new folder http
and some .go
file. In this package create a struct named handler
which will hold a field of the fact repository.
Example:
type FactsHandler struct {
FactRepo *fact.Repository
}
Create methods for handling the request
func (h *FactsHandler) Ping(w http.ResponseWriter, r *http.Request)
func (h *FactsHandler) Facts(w http.ResponseWriter, r *http.Request)
Move the anonymous
http.HandleFunc
from main and put as this struct's methods (these with the signaturefunc(w http.ResponseWriter, r *http.Request)
)
In main, init FactsHandler
struct with the factRepo
.
You may noticed that http
is already taken as a package name by net/http
. You're not wrong, we can't use both packageS in one file and still call each package http
. But we can rename the import name:
package main
import (
"net/http"
facthttp "github.com/FTBpro/go-workshop/coolfacts/exercise8/http"
)
func main() {
handlerer := facthttp.FactsHandler{
FactRepo: &factsRepo,
}
http.HandleFunc("/ping", handlerer.Ping)
}
Creating a package
http
may not be the best idea. If we create a package with an infrastructure name (like sql, mentalfloss...) we need to make sure we import it only from main.
Use go channel and ticker for updating the fact inventory
Every specified time a ticker will send a signal using a channel
(go built-in) that will trigger fetch new fact from provider (mentalfloss)
- Init a
ctx, cancelFunc := context.WithCancel(context.Background())
(use the cancel func in the end of the run to terminate the go routing execution)-
context.WithCancel(context.Background())
returns a context which have a done channel on it that can be used as follows :<-ctx.Done()
to signal termintation. -
context.WithCancel(context.Background())
also returns a cancel func that can be called at the end of the run - it will send a message to thectx.Done()
channel. -
The call to the
cancelFunc()
can be done usingdefer
which invokes whatever defined after it at the end of the function that containes the declaration:
func example(){ ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() //defined but not invoked doSomething(ctx) //This is the end of the function so defer is invoked }
-
- Add a function - func updateFactsWithTicker(ctx context.Context, updateFunc func() error)
- (Outside from updateFactsWithTicker) Create the updateFunc from step 7.2. that updates the repository from an external provider
- (Within the updateFactsWithTicker) Create a time.NewTicker
- (Within the updateFactsWithTicker) Add a go routine with a function that accepts the context
- Inside the function add an endless loop that will select from the channel (the ticker channel and the context one)
- If the ticker channel (ticker.C) was selected - use the given updateFunc to update repository
- If the context channel (context.Done()) was selected -return (it means the main closed the context)
- Inside the function add an endless loop that will select from the channel (the ticker channel and the context one)
Decouple and break dependancies using interfasces
We need to encapsulate the process of storing the facts. You can think of this package like some kind of an interactor between the actual caching/persistent layer to the application domain.
In this example we will create a package dedicated for storing the facts in a simple slice
. The package we'll create will be called inmem
. (Accordinagly, if we will use SQL, we will create package sql
)
After creating packe inmem
, we need to move the repository
functionality currently in package fact
.
For exporting it we will use function NewFactRepository. The consumer (main) will call it via inmem.NewFactRepositry()
pkg inmem
type factRepository struct {
facts []fact.Fact
}
func NewFactRepository() *factRepository {
return &factRepository{}
}
func (r *factRepository) Add(f fact.Fact) {
// code
}
func (r *factRepository) GetAll() []fact.Fact {
// code
}
We need some kind of "service" to handle our update logic. This service will be initialized with the repository and the mentalfloss provider, and have an Update
method.
We better not limit ourselves to only mentalfloss and only in memory cache. We can do that by using interfaces instead the concrete types.
Example:
In package fact
, declare two interfaces:
package fact
type Provider interface {
Facts() ([]Fact, error)
}
type Repository interface {
Add(f Fact)
GetAll() []Fact
}
The service will be a struct which we will export using NewService function that takes a Provider
and a Repository
.
The service will have UpdateFacts
method that take no parameters.
For example
// continue package fact
type service struct {
provider Provider
repository Repository
}
func NewService(s Repository, r Provider) *service {
// code
}
func (s *service) UpdateFacts() error {
// code
}
Although the service is updating the facts, it doesn't know from which provider or what is the persistent layer. That means we could easily replace inmem with a db, switch providers, and add middlewares (decorators) for logging and other stuff.
Instead of a service, we could just create an exported function
fact.UpdateFacts(p Provider, s Repository) error
, which would achieve the same goal and have some advantages, same asupdateFactsFunc
principle in exercise 7.
By now you can see that we broke our custom http
package. This is because *fact.Repository
isn't defined anymore (we moved the repository to inmem
).
We'll use here the interface principle as well. We'll declare same interface in our http
:
package http
type FactRepository interface {
Add(f fact.Fact)
GetAll() []fact.Fact
}
type factsHandler struct {
factRepo FactRepository
}
func NewFactsHandler(factRepo FactRepository) *factsHandler {
return &factsHandler{
factRepo: factRepo,
}
}
Same as in the service, we can initialize the handler with a different persistent layer, add middlewares and more
For making it more readable (perhaps), we will use the name provider
instead of the mentalfloss
struct. This is a naming convention when we are interacting with external services.
package mentalfloss
type provider struct{}
func NewProvider() *provider {
return &provider{}
}
The consumer will now use it by calling mentalfloss.NewProvider()
All left to do is to replcae our way we initialize our dependancies in main.
instead of initializing the factRepo
like this:
factsRepo := fact.Repository{}
we will initialize it with our inmem
package:
factsRepo := inmem.NewFactRepository()
Instead of using the updateFunc
from exercise 7, we'll use our fact.NewService
:
mentalflossProvider := mentalfloss.NewProvider()
service := fact.NewService(mentalflossProvider, &factsRepo)
Now we will just the service.UpdateFacts
method to update the repository, and to use with the ticker and we're done 🥂
- Effective Go - a detailed set of the specialities and atyleguides for Go
- Code review comments - common comments made during reviews of Go code
- Idiomatic GO - general guidelines of what not to do
- Go blog styleguides about naming packages
- Blog post by Dave Cheney wbout why we shouldn't use
util
orcommon