Backend development


Gregory Vinčić

Backend development

History

Robert Griesemer, Ken Thompson and Rob Pike are the original authors of the Go language which is currently supervised by Russ Cox. Ian wrote the first compiler as a frontend to gcc. Read more on their (FAQ)


The story

  1960       1970          1980          1990        2000      2007-2009

                                                +- Javascript  --+
                                               /                  \
                +----- C -----------------+---+ PHP/Perl        ---+
               /                           \                        \
- Algol ------+                             +-- Java             ----+- Go
               \                                                    /
                \       +--- Modula ---+-- Python               -- +
                 \     /                                          /
                  +---+----------+         o-- Ada 95            /
                  |               \                             /
              Pascal               +---- Oberon             ---+

Quick install

Download and install Go in the $HOME directory.

$ cd $HOME
$ wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
$ tar xvfz go1.22.0.linux-amd64.tar.gz
$ export PATH="$PATH:$HOME/go/bin"
$ go version
go version go1.22.0 linux/amd64

$ ls -1 $HOME/go/bin
go
gofmt

For more information refer to "Download and install".

Project structure

$ cd neon; tree/
. ├── go.mod ├── main.go └── main_test.go 0 directories, 3 files
"A basic Go package has all its code in the project’s root directory."

go.dev/doc/modules/layout
$ cd neon/; tree
. ├── cmd │   └── neon │   ├── main.go │   └── main_test.go ├── booking.go ├── go.mod ├── hotel.go └── room.go 2 directories, 6 files
$ cd neon/; tree
. ├── cmd │   └── neon │   ├── booking.go │   ├── hotel.go │   ├── main.go │   ├── main_test.go │   └── room.go ├── internal │   ├── crypt │   ├── mqtt.go │   └── postgres.go └── go.mod 4 directories, 8 files
"Server project"

go.dev/doc/modules/layout#server-project
$ cd neon/; tree
. ├── cmd │   └── neon │   ├── main.go │   └── main_test.go ├── internal │   ├── crypt │   ├── mqtt.go │   └── postgres.go ├── booking.go ├── go.mod ├── hotel.go ├── room.go └── system.go 4 directories, 9 files
"Don't waste a good name"

Gregory Vinčić

Concepts

In Go; modules are used to group packages and functions.

"You can collect related packages into modules, then publish the modules for other developers to use."

go.dev/doc/modules/developing

An interface describes behavior.

"Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here."

go.dev/doc/effective_go#interfaces_and_types
Concepts

Module

Modules are initiated using the repository URL.

$ go mod init github.com/gregoryv/uptime

This can then be used

$ go get github.com/gregoryv/uptime[@VERSION]

Find modules on pkg.go.dev

Sharing code with others requires dependency management.


Concepts

Package

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/gregoryv/uptime"
  6. )
  7. func main() {
  8. start := time.Now()
  9. dur := uptime.Since(start)
  10. fmt.Println(dur.String())
  11. }

"... package name should be good: short, concise, evocative. ...Use the package structure to help you choose good names. "

go.dev/doc/effective_go#package-names

Concepts

Interface


"Interfaces with only one or two methods are common in Go code, and are usually given a name derived from the method, such as io.Writer for something that implements Write."

go.dev/doc/effective_go#interfaces_and_types
  1. package fmt
  2. type Stringer interface {
  3. String() string
  4. }

  1. func main() {
  2. fmt.Println(painted(color(2)))
  3. }
  4. func painted(val fmt.Stringer) string {
  5. return fmt.Sprintf("%T.String(): %s", val, val.String())
  6. }
  7. type color int
  8. func (c color) String() string {
  9. switch c {
  10. case 1:
  11. return "red"
  12. case 2:
  13. return "green"
  14. default:
  15. return "white"
  16. }
  17. }

HTTP

Respond to HTTP requests in Go by implementing the http.Handler interface.

  1. package http // import "net/http"
  2. // A Handler responds to an HTTP request. Handler.ServeHTTP should
  3. // write reply headers and data to the ResponseWriter and then return.
  4. // ...
  5. type Handler interface {
  6. ServeHTTP(ResponseWriter, *Request)
  7. }
  8. // The HandlerFunc type is an adapter to allow the use of ordinary
  9. // functions as HTTP handlers. If f is a function with the appropriate
  10. // signature, HandlerFunc(f) is a Handler that calls f.
  11. type HandlerFunc func(ResponseWriter, *Request)
  12. func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  13. f(w, r)
  14. }
HTTP

Direct

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. err := http.ListenAndServe(":8080", http.HandlerFunc(sayHello))
  9. if err != nil {
  10. log.Fatal(err)
  11. }
  12. }
  13. func sayHello(w http.ResponseWriter, r *http.Request) {
  14. fmt.Fprint(w, "Hello, world!")
  15. }

Direct design uses named functions as http handlers, implementing the http.HandlerFunc interface


$ go run helloworld.go
HTTP

Closure

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. err := http.ListenAndServe(":8080", sayHello())
  9. if err != nil {
  10. log.Fatal(err)
  11. }
  12. }
  13. func sayHello() http.HandlerFunc {
  14. txt := "Hello, world!"
  15. return func(w http.ResponseWriter, r *http.Request) {
  16. fmt.Fprint(w, txt)
  17. }
  18. }

Closure design uses named functions returning a http.HandlerFunc.


HTTP

Method

  1. func main() {
  2. handler := &Greeter{
  3. // complex setup
  4. }
  5. err := http.ListenAndServe(":8080", handler)
  6. if err != nil {
  7. log.Fatal(err)
  8. }
  9. }
  10. type Greeter struct {
  11. // complex relations, eg. database
  12. }
  13. func (g *Greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  14. fmt.Fprint(w, "Hello, world!")
  15. }

Method design declares types that implement http.Handler


HTTP

Combo

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. ctl := Controller{
  9. // complex setup
  10. }
  11. if err := http.ListenAndServe(":8080", ctl.sayHello()); err != nil {
  12. log.Fatal(err)
  13. }
  14. }
  15. type Controller struct{}
  16. func (c *Controller) sayHello() http.HandlerFunc {
  17. return func(w http.ResponseWriter, r *http.Request) {
  18. fmt.Fprint(w, "Hello, world!")
  19. }
  20. }

Combo design declares types with multiple methods with http.HandlerFunc signature or return one (closure).


Route

For systems serving many resources

  1. package http
  2. // ServeMux is an HTTP request multiplexer. It matches the URL of each
  3. // incoming request against a list of registered patterns and calls
  4. // the handler for the pattern that most closely matches the URL.
  5. type ServeMux struct {
  6. // Has unexported fields.
  7. }
multiplexer
      n 1: a device that can interleave two or more activities
Route

Naive

  1. func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  2. switch {
  3. case r.URL.Path == "/bye" && r.Method == "GET":
  4. c.sayGoodbye(w, r)
  5. case r.URL.Path == "/" && r.Method == "GET":
  6. c.sayHello(w, r)
  7. default:
  8. http.Error(w, "routing failed", http.StatusBadRequest)
  9. }
  10. }

Manual routing.

Error prone and complex.

Route

Default http.ServeMux

  1. func main() {
  2. var ctl Controller
  3. http.HandleFunc("GET /bye", ctl.sayGoodbye)
  4. http.HandleFunc("GET /", ctl.sayHello) // everything else
  5. if err := http.ListenAndServe(":8080", nil); err != nil {
  6. log.Fatal(err)
  7. }
  8. }
  9. type Controller struct{}
  10. func (c *Controller) sayHello(w http.ResponseWriter, r *http.Request) {
  11. fmt.Fprint(w, "Hello, world!")
  12. }
  13. func (c *Controller) sayGoodbye(w http.ResponseWriter, r *http.Request) {
  14. fmt.Fprint(w, "Goodbye, world!")
  15. }

Use http singleton ServeMux.

Route

Your own http.ServeMux

  1. func main() {
  2. var ctl Controller
  3. mux := http.NewServeMux()
  4. mux.HandleFunc("GET /bye", ctl.sayGoodbye)
  5. mux.HandleFunc("GET /", ctl.sayHello)
  6. if err := http.ListenAndServe(":8080", mux); err != nil {
  7. log.Fatal(err)
  8. }
  9. }
  10. type Controller struct{}
  11. func (c *Controller) sayHello(w http.ResponseWriter, r *http.Request) {
  12. fmt.Fprint(w, "Hello, world!")
  13. }
  14. func (c *Controller) sayGoodbye(w http.ResponseWriter, r *http.Request) {
  15. fmt.Fprint(w, "Goodbye, world!")
  16. }

Create your own http.ServeMux.

Route

Path value

  1. package main
  2. import (
  3. "encoding/json"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. // available people, ie. our data store
  9. people := []Person{
  10. {Id: "p1", Name: "John"},
  11. {Id: "p2", Name: "Jane"},
  12. }
  13. mux := http.NewServeMux()
  14. mux.Handle("GET /person/{id}", servePersonById(people))
  15. if err := http.ListenAndServe(":8080", mux); err != nil {
  16. log.Fatal(err)
  17. }
  18. }
  1. func servePersonById(people []Person) http.HandlerFunc {
  2. return func(w http.ResponseWriter, r *http.Request) {
  3. id := r.PathValue("id")
  4. for _, person := range people {
  5. if person.Id == id {
  6. json.NewEncoder(w).Encode(person)
  7. return
  8. }
  9. }
  10. // ... handle not found
  11. }
  12. }
  13. type Person struct {
  14. Id string
  15. Name string
  16. }

Encode

From the dictionary

encode
  
    <algorithm, hardware> To convert {data} or some physical
    quantity into a given format.  E.g. {uuencode}.

In Go this equals to marshal type X to []byte

Encode

encoding/json

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. )
  6. func main() {
  7. user := Contact{
  8. Firstname: "John",
  9. Lastname: "Doe",
  10. Age: 23,
  11. Alive: true,
  12. }
  13. data, _ := json.Marshal(user)
  14. fmt.Print(string(data))
  15. }
  16. type Contact struct {
  17. Firstname string
  18. Lastname string
  19. Age int
  20. Alive bool
  21. }
$ go run ./examples/encoding.go
{"Firstname":"John","Lastname":"Doe","Age":23,"Alive":true}
Encode

Nice json

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. )
  6. func main() {
  7. user := Contact{
  8. Firstname: "John",
  9. Lastname: "Doe",
  10. Age: 23,
  11. Alive: true,
  12. }
  13. data, _ := json.MarshalIndent(user, "", " ")
  14. fmt.Print(string(data))
  15. }
  16. type Contact struct {
  17. Firstname string
  18. Lastname string
  19. Age int
  20. Alive bool
  21. }
$ go run ./examples/nicejson.go
{ "Firstname": "John", "Lastname": "Doe", "Age": 23, "Alive": true }
Encode

Struct tags

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. )
  6. func main() {
  7. user := Contact{
  8. Firstname: "John",
  9. Lastname: "Doe",
  10. }
  11. data, _ := json.MarshalIndent(user, "", " ")
  12. fmt.Print(string(data))
  13. }
  14. type Contact struct {
  15. Firstname string `json:"firstname"` // change name
  16. Lastname string `json:"-"` // ignore this field
  17. }

Control naming of fields, eg. lowercase names.

$ go run ./examples/fieldtags.go
{ "firstname": "John" }
Encode

Complex struct

  1. type Contact struct {
  2. Firstname string `json:"firstname"` // change name
  3. Lastname string `json:"-"` // ignore this field
  4. *Address `json:"address"`
  5. }
  6. type Address struct {
  7. Street string `json:"street"`
  8. Number uint `json:"no"` // rename entirely
  9. PostalCode int `json:"postal_code,omitempty"` // skip empty value
  10. }
$ go run ./examples/complex_struct.go
{ "firstname": "John", "address": { "street": "Lexington road", "no": 137 } }

Test

main_test.go
  1. package main
  2. import "testing"
  3. func TestSum(t *testing.T) {
  4. if s := Sum(1, 2); s != 3 {
  5. t.Errorf("got %v, expect 3", s)
  6. }
  7. }
main.go
  1. func Sum(a, b int) int {
  2. return a + b
  3. }
.
├── go.mod
├── main.go
└── main_test.go

0 directories, 3 files
$ go test -v .
=== RUN TestSum --- PASS: TestSum (0.00s) PASS ok github.com/gregoryv/project 0.001s
Test

http/httptest

  1. package main
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. )
  7. func Test_sayHello(t *testing.T) {
  8. // prepare response recorder and request
  9. w := httptest.NewRecorder()
  10. r := httptest.NewRequest("GET", "/", http.NoBody)
  11. // call handler
  12. handler := sayHello() // using the closure design
  13. handler(w, r)
  14. // check result
  15. resp := w.Result()
  16. if v := resp.StatusCode; v != 200 {
  17. t.Error(resp.Status)
  18. }
  19. }

Quick unit test of specific handlers using httptest.Recorder

  1. setup
  2. call handler
  3. check response
Test

Coverage

$ go test -v -cover .
=== RUN Test_sayHello --- PASS: Test_sayHello (0.00s) PASS coverage: 60.0% of statements ok goback/examples/httptest 0.002s coverage: 60.0% of statements
main_test.go
  1. func Test_sayHello(t *testing.T) {
  2. // prepare response recorder and request
  3. w := httptest.NewRecorder()
  4. r := httptest.NewRequest("GET", "/", http.NoBody)
  5. // call handler
  6. handler := sayHello() // using the closure design
  7. handler(w, r)
  8. // check result
  9. resp := w.Result()
  10. if v := resp.StatusCode; v != 200 {
  11. t.Error(resp.Status)
  12. }
  13. }
main.go
  1. func main() {
  2. if err := http.ListenAndServe(":8080", sayHello()); err != nil {
  3. log.Fatal(err)
  4. }
  5. }
  6. func sayHello() http.HandlerFunc {
  7. txt := "Hello, world!"
  8. return func(w http.ResponseWriter, r *http.Request) {
  9. fmt.Fprint(w, txt)
  10. }
  11. }

View

Serverside rendered HTML for several use cases

  • The obvious, you love the old school
  • API docs
  • Server stats
  • Performance, improve user experience and load times
View

html/template

  1. package main
  2. import (
  3. "html/template"
  4. "log"
  5. "os"
  6. )
  7. func main() {
  8. tpl, err := template.ParseFiles("examples/index.html")
  9. if err != nil {
  10. log.Fatal(err)
  11. }
  12. model := map[string]any{
  13. "Title": "<script>alert('WOW');</script>",
  14. }
  15. if err := tpl.Execute(os.Stdout, model); err != nil {
  16. log.Fatal(err)
  17. }
  18. }
  1. <html>
  2. <body>
  3. <h1>{{.Title}}</h1>
  4. </body>
  5. </html>
$ go run examples/htmltpl.go
<html> <body> <h1>&lt;script&gt;alert(&#39;WOW&#39;);&lt;/script&gt;</h1> </body> </html>
View

embed

  1. package main
  2. import (
  3. "embed"
  4. "html/template"
  5. "log"
  6. "os"
  7. )
  8. func main() {
  9. model := map[string]any{
  10. "Title": "<script>alert('WOW');</script>",
  11. }
  12. err := tpl.ExecuteTemplate(os.Stdout, "index.html", model)
  13. if err != nil {
  14. log.Fatal(err)
  15. }
  16. }
  17. var tpl *template.Template = template.Must(
  18. template.ParseFS(assets, "assets/*.html"),
  19. )
  20. //go:embed assets/*.html
  21. var assets embed.FS

"Package embed provides access to files embedded in the running Go program."

pkg.go.dev/embed

$ go run examples/embed.go
<html> <body> <h1>&lt;script&gt;alert(&#39;WOW&#39;);&lt;/script&gt;</h1> </body> </html>

Complete example

main.go
  1. func main() {
  2. h := &Hotel{
  3. Name: "Purple glow",
  4. Rooms: []Room{
  5. {Number: "1"},
  6. {Number: "2"},
  7. {Number: "3"},
  8. },
  9. }
  10. mux := http.NewServeMux()
  11. mux.Handle("POST /room/{number}", BookRoom(h.Rooms))
  12. mux.Handle("GET /room", ServeRooms(h.Rooms))
  13. mux.Handle("GET /", ServeHotel(h))
  14. if err := http.ListenAndServe(":8080", mux); err != nil {
  15. log.Fatal(err)
  16. }
  17. }
  18. type Hotel struct {
  19. Name string `json:"name"`
  20. Rooms []Room `json:"-"` // rooms are rendered separately
  21. }
  22. type Room struct {
  23. Number string `json:"number"` // using string for simplicty
  24. Booked bool `json:"booked"`
  25. }
main.go (continued)
  1. func BookRoom(rooms []Room) http.HandlerFunc {
  2. return func(w http.ResponseWriter, r *http.Request) {
  3. number := r.PathValue("number")
  4. // update room state
  5. for i, room := range rooms {
  6. if room.Number != number {
  7. continue
  8. }
  9. rooms[i].Booked = true
  10. }
  11. json.NewEncoder(w).Encode(rooms)
  12. }
  13. }
  14. func ServeRooms(rooms []Room) http.HandlerFunc {
  15. return func(w http.ResponseWriter, r *http.Request) {
  16. json.NewEncoder(w).Encode(rooms)
  17. }
  18. }
  19. func ServeHotel(h *Hotel) http.HandlerFunc {
  20. return func(w http.ResponseWriter, r *http.Request) {
  21. json.NewEncoder(w).Encode(h)
  22. }
  23. }
$ cd neon/; tree
. ├── go.mod ├── main.go └── main_test.go 0 directories, 3 files
main_test.go
  1. func Test_main(t *testing.T) {
  2. go main()
  3. <-time.After(time.Millisecond) // bad test strategy
  4. cases := []struct {
  5. exp int // expected status code
  6. *http.Request
  7. }{
  8. {200, newRequest(t, "POST", "http://localhost:8080/room/1")},
  9. {200, newRequest(t, "GET", "http://localhost:8080/room")},
  10. {200, newRequest(t, "GET", "http://localhost:8080/")},
  11. }
  12. for _, c := range cases {
  13. resp, err := http.DefaultClient.Do(c.Request)
  14. if err != nil {
  15. t.Fatal(err)
  16. }
  17. if resp.StatusCode != c.exp {
  18. t.Error(resp.Status)
  19. }
  20. }
  21. }
  22. func newRequest(t *testing.T, method, path string) *http.Request {
  23. r, err := http.NewRequest(method, path, http.NoBody)
  24. if err != nil {
  25. t.Fatal(err)
  26. }
  27. return r
  28. }
$ go test -v -cover .
=== RUN Test_main --- PASS: Test_main (0.00s) PASS coverage: 94.4% of statements ok github.com/ 0.006s ...

Summary

Concepts covered in this presentation

Go language/structure
modulefor dependency management
packagegrouping related code
interfacebehavior abstraction

Package
net/httphandling requests and routing
net/http/httptestutilities for HTTP testing
testingautomated testing of Go packages
encoding/jsonmarshaling datatypes to JSON
html/templaterendering HTML
embedprovides access to embedded files

Next time


Backend development
  1. Go concepts
  2. Project structure
  3. HTTP routing
  4. Handler design
  5. Test handlers
  6. JSON Encoding
General problems
  1. Test driven development
  2. Package testdata
  3. Documentation
  4. Logging
  5. Error handling
  6. Database IO
Concurrency problems
  1. Concurrency
  2. Benchmark
  3. Testing strategy
  4. ... more