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.ResponseWriteryou can usefmt.Fprintf, and in case of an error you can usehttp.Errorfunction
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.factsfield
- 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
ResponseWriterin a JSON format
Use
json.Marshalto 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
deferstatement 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
factpackage - Move mentalfloss functionality into
mentalflosspackage - Move the http handlers into a into
httppackage (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 factMove the fact and the repository structs into that file.
Make sure that the struct
Factis 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
factis in another package. You will need to import your fact package And replacefactwithfact.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.HandleFuncfrom 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
httpmay 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 usingdeferwhich 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 asupdateFactsFuncprinciple 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
utilorcommon
