Requirement of environment:

golang
Linux localhost.localdomain 3.10.0-514.26.2.el7.x86_64
CentOS Linux release 7.3.1611 (Core)

Apr. 22

  • Create a work folder first for testing
  • Basic “Hello World” program (main.go): ```go package main

import ( “log” “net/http” )

func main() { //registers a function to a path //a convenient method http.HandleFunc(“/“, func(http.ResponseWriter, *http.Request) { log.Println(“Hello World”) })

  1. //":9090" means listen to any port 9090 from any nodes
  2. http.ListenAndServe(":9090", nil)

}

  1. To test:
  2. ```bash
  3. # on the first terminal
  4. go run main.go
  5. # open another terminal
  6. curl -v localhost:9090

Then you will see the first terminal prints “Hello World”.

  • Extend path above by changing the path, add:
    1. http.HandleFunc("/goodbye", func(http.ResponseWriter, *http.Request) {
    2. log.Println("Goodbye World")
    3. })
    To test: ```go //on the first terminal go run main.go

//open another terminal curl -v localhost:9090/goodbye

  1. Then you will see the first terminal prints "Goodbye World".
  2. ---
  3. <a name="dQdxL"></a>
  4. ### Apr. 23
  5. - Extract data
  6. - Import "io/ioutil" && ioutil.ReadAll((*http.Request).Body)
  7. - example:
  8. ```go
  9. package main
  10. import (
  11. "io/ioutil"
  12. "log"
  13. "net/http"
  14. )
  15. func main() {
  16. http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
  17. log.Println("Hello World")
  18. d, _ := ioutil.ReadAll(r.Body)
  19. //fmt is on the server
  20. log.Printf("Data %s\n", d)
  21. })
  22. //":9090" means listen to any port 9090 from any nodes
  23. http.ListenAndServe(":9090", nil)
  24. }

To test:

  1. //on the first terminal
  2. go run main.go
  3. //open another terminal
  4. curl -v -d 'Here is data' localhost:9090

Then you will see the first terminal prints ‘Data Here is data’.

  • Response of the server:
  • example ```cpp package main

import ( “fmt” “io/ioutil” “log” “net/http” )

func main() { http.HandleFunc(“/“, func(rw http.ResponseWriter, r *http.Request) { log.Println(“Hello World”)

  1. d, _ := ioutil.ReadAll(r.Body)
  2. //fmt is server to user
  3. fmt.Fprintf(rw, "Hello %s\n", d)
  4. })
  5. //":9090" means listen to any port 9090 from any nodes
  6. http.ListenAndServe(":9090", nil)

}

  1. To test:
  2. ```bash
  3. # on the first terminal
  4. go run main.go
  5. # open another terminal
  6. # -v is for details, to see the response more clearly this time, ignore it
  7. curl -d 'Nic' localhost:9090

Then you will see another terminal prints ‘Hello Nic’.

  • Handle error ```go d, error := ioutil.ReadAll(r.Body)

if err != nil { rw.WriteHeader(http.StatusBadRequest) rw.Write([]byte(“Oops”)) return }

//OR if err != nil { http.Error(rw, “Oops”, http.StatusBadRequest) return }

  1. <a name="avEPH"></a>
  2. ### Apr. 25
  3. - Made a package as handlers/hello.go
  4. ```go
  5. package handlers
  6. import (
  7. "fmt"
  8. "io/ioutil"
  9. "log"
  10. "net/http"
  11. )
  12. type Hello struct {
  13. l *log.Logger
  14. }
  15. func NewHello(l *log.Logger) *Hello {
  16. return &Hello{l}
  17. }
  18. //ServeHTTP is mandatory
  19. func (h *Hello) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
  20. h.l.Println("Hello World")
  21. d, err := ioutil.ReadAll(r.Body)
  22. if err != nil {
  23. http.Error(rw, "Oops", http.StatusBadRequest)
  24. return
  25. }
  26. fmt.Fprintf(rw, "Hello %s\n", d)
  27. }
  • And use it in main.go before ```go package main

import ( “log” “net/http” “os”

  1. "microservice/handlers"

)

func main() { l := log.New(os.Stdout, “product-api”, log.LstdFlags) hh := handlers.NewHello(l)

  1. sm := http.NewServeMux()
  2. sm.Handle("/", hh)
  3. http.ListenAndServe(":9090", sm)

}

  1. (Doesn't work, back to set up go.mod to make it as a project)
  2. <a name="gDgS2"></a>
  3. ### Apr. 26
  4. - initialized go.mod in microservice/
  5. ```bash
  6. go mod init microservice
  • now I can apply package handlers.
  • Use these lines to test codes on Apr. 25: ```bash

    on the first terminal

    go run main.go

open another terminal

-v is for details, to see the response more clearly this time, ignore it

curl -d ‘Nic’ localhost:9090

  1. Server prints "Hello World", and responses "Hello Nic".
  2. - handle '/goodbye', set up goodbye.go in handlers folder:
  3. ```go
  4. package handlers
  5. import (
  6. "log"
  7. "net/http"
  8. )
  9. type Goodbye struct {
  10. l *log.Logger
  11. }
  12. func NewGoodbye(l *log.Logger) *Goodbye {
  13. return &Goodbye{l}
  14. }
  15. //ServeHTTP is mandatory
  16. func (g *Goodbye) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
  17. rw.Write([]byte("Byeee"))
  18. }
  • Then update main.go: ```go package main

import ( “log” “net/http” “os”

  1. "microservice/handlers"

)

func main() { l := log.New(os.Stdout, “product-api”, log.LstdFlags) hh := handlers.NewHello(l) gh := handlers.NewGoodbye(l)

  1. //integrate handlers
  2. sm := http.NewServeMux()
  3. sm.Handle("/", hh)
  4. sm.Handle("/goodbye", gh)
  5. http.ListenAndServe(":9090", sm)

}

  1. - To test:
  2. ```bash
  3. # on the first terminal
  4. go run main.go
  5. # open another terminal
  6. curl -v localhost:9090/goodbye

Server responses “Byeee”.

Apr. 27

  • Write a custom server: ```go package main

import ( “log” “net/http” “os” “time”

  1. "microservice/handlers"

)

func main() { l := log.New(os.Stdout, “product-api”, log.LstdFlags) hh := handlers.NewHello(l) gh := handlers.NewGoodbye(l)

  1. sm := http.NewServeMux()
  2. sm.Handle("/", hh)
  3. sm.Handle("/goodbye", gh)
  4. //create a custom server
  5. s := &http.Server{
  6. Addr: ":9090",
  7. Handler: sm,
  8. ReadTimeout: 120 * time.Second,
  9. IdleTimeout: 1 * time.Second,
  10. WriteTimeout: 1 * time.Second,
  11. }
  12. s.ListenAndServe()

}

  1. This kind of method to setup server can integrate handlers and decide variables such as timeout. The workout is same as codes on Apr. 26.
  2. - notice of interrupt/kill and set graceful shutdown:
  3. ```go
  4. package main
  5. import (
  6. "context"
  7. "log"
  8. "net/http"
  9. "os"
  10. "os/signal"
  11. "time"
  12. "microservice/handlers"
  13. )
  14. func main() {
  15. l := log.New(os.Stdout, "product-api", log.LstdFlags)
  16. hh := handlers.NewHello(l)
  17. gh := handlers.NewGoodbye(l)
  18. sm := http.NewServeMux()
  19. sm.Handle("/", hh)
  20. sm.Handle("/goodbye", gh)
  21. //create a custom server
  22. s := &http.Server{
  23. Addr: ":9090",
  24. Handler: sm,
  25. ReadTimeout: 120 * time.Second,
  26. IdleTimeout: 1 * time.Second,
  27. WriteTimeout: 1 * time.Second,
  28. }
  29. //go routine - spread an extra branch as the independent server
  30. go func() {
  31. err := s.ListenAndServe()
  32. if err != nil {
  33. l.Fatal(err)
  34. }
  35. }()
  36. //print the signal of interrupt
  37. sigChan := make(chan os.Signal)
  38. signal.Notify(sigChan, os.Interrupt)
  39. signal.Notify(sigChan, os.Kill)
  40. sig := <-sigChan
  41. l.Println("Received terminate, graceful shutdown", sig)
  42. //shutdown after everything is finished and wait for 30 more seconds
  43. tc, _ := context.WithTimeout(context.Background(), 30*time.Second)
  44. s.Shutdown(tc)
  45. }

To test:

  1. # on server side terminal
  2. go run main.go
  3. ^C

Shows:

  1. product-api2020/04/28 03:16:54 Received terminate, graceful shutdown interrupt

Apr. 28

REST(Representational State Transfer)ful:

The purpose is to let different softwares/programs can communicate in a network easily.
To conclude REST: https://restfulapi.net/

Product API:

  • Build a product API: the example of the video is for a coffee shop
  • we have a new package “data”, and products.go: ```go package data

import “time”

type Product struct { ID int Name string Description string Price float32 SKU string CreatedOn string UpdatedOn string DeletedOn string }

//getter func GetProducts() []*Product { return productList }

var productList = []*Product{ &Product{ ID: 1, Name: “Latte”, Description: “Frothy milky coffee”, Price: 2.45, SKU: “abc323”, CreatedOn: time.Now().UTC().String(), UpdatedOn: time.Now().UTC().String(), }, &Product{ ID: 2, Name: “Espresso”, Description: “Short and strong coffee without milk”, Price: 1.99, SKU: “fjd34”, CreatedOn: time.Now().UTC().String(), UpdatedOn: time.Now().UTC().String(), }, }

  1. - Then in folder "handler", also setup a product.go:
  2. ```go
  3. package handlers
  4. import (
  5. "encoding/json"
  6. "log"
  7. "microservice/data"
  8. "net/http"
  9. )
  10. type Products struct {
  11. l *log.Logger
  12. }
  13. func NewProducts(l *log.Logger) *Products {
  14. return &Products{l}
  15. }
  16. func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
  17. lp := data.GetProducts()
  18. //read data as json
  19. d, err := json.Marshal(lp)
  20. if err != nil {
  21. http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)
  22. }
  23. rw.Write(d)
  24. }
  • Finally, modify main.go for Products:

    1. //...
    2. ph := handlers.NewProducts(l)
    3. sm := http.NewServeMux()
    4. sm.Handle("/", ph)
    5. //...

    To test: ```bash

    on the first terminal

    go run main.go

open another terminal

curl localhost:9090

  1. The output looks like:
  2. ```bash
  3. [{"ID":1,"Name":"Latte","Description":"Frothy milky coffee","Price":2.45,"SKU":"abc323","CreatedOn":"2020-04-28 23:35:17.865468881 +0000 UTC","UpdatedOn":"2020-04-28 23:35:17.865480655 +0000 UTC","DeletedOn":""},{"ID":2,"Name":"Espresso","Description":"Short and strong coffee without milk","Price":1.99,"SKU":"fjd34","CreatedOn":"2020-04-28 23:35:17.865482031 +0000 UTC","UpdatedOn":"2020-04-28 23:35:17.865483018 +0000 UTC","DeletedOn":""}]

It’s too long and in the same line, so that we install jq to handle JSON data:

  1. yum install jq
  2. # on the first terminal
  3. go run main.go
  4. # open another terminal
  5. curl localhost:9090 | jq

This time, the output looks way better:

  1. % Total % Received % Xferd Average Speed Time Time Time Current
  2. Dload Upload Total Spent Left Speed
  3. 100 442 100 442 0 0 371k 0 --:--:-- --:--:-- --:--:-- 431k
  4. [
  5. {
  6. "ID": 1,
  7. "Name": "Latte",
  8. "Description": "Frothy milky coffee",
  9. "Price": 2.45,
  10. "SKU": "abc323",
  11. "CreatedOn": "2020-04-28 23:35:17.865468881 +0000 UTC",
  12. "UpdatedOn": "2020-04-28 23:35:17.865480655 +0000 UTC",
  13. "DeletedOn": ""
  14. },
  15. {
  16. "ID": 2,
  17. "Name": "Espresso",
  18. "Description": "Short and strong coffee without milk",
  19. "Price": 1.99,
  20. "SKU": "fjd34",
  21. "CreatedOn": "2020-04-28 23:35:17.865482031 +0000 UTC",
  22. "UpdatedOn": "2020-04-28 23:35:17.865483018 +0000 UTC",
  23. "DeletedOn": ""
  24. }
  25. ]

Add struct tags/annotations for elements:

  1. //...
  2. type Product struct {
  3. ID int `json:"id"`
  4. Name string `json:"name"`
  5. Description string `json:"description"`
  6. Price float32 `json:"price"`
  7. SKU string `json:"sku"`
  8. CreatedOn string `json:"-"`
  9. UpdatedOn string `json:"-"`
  10. DeletedOn string `json:"-"`
  11. }
  12. //...

This way affects what jq shows, we test in the same way and get:

  1. # on the first terminal
  2. go run main.go
  3. # open another terminal
  4. curl localhost:9090 | jq
  5. % Total % Received % Xferd Average Speed Time Time Time Current
  6. Dload Upload Total Spent Left Speed
  7. 100 196 100 196 0 0 31106 0 --:--:-- --:--:-- --:--:-- 32666
  8. [
  9. {
  10. "id": 1,
  11. "name": "Latte",
  12. "description": "Frothy milky coffee",
  13. "price": 2.45,
  14. "sku": "abc323"
  15. },
  16. {
  17. "id": 2,
  18. "name": "Espresso",
  19. "description": "Short and strong coffee without milk",
  20. "price": 1.99,
  21. "sku": "fjd34"
  22. }
  23. ]

Apr. 29

GET Method:

  • modify /data/products.go ```go //… //define a type type Products []*Product

//encode Product list into JSON func (p *Products) ToJSON(w io.Writer) error { e := json.NewEncoder(w) return e.Encode(p) }

//getter, changed the signature func GetProducts() Products { return productList } //…

  1. This step defines a type to simplify []*Product as Products, and allows converting Products into JSON for some methods.
  2. - Modify /handlers/product.go to apply ToJSON above:
  3. ```go
  4. func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
  5. lp := data.GetProducts()
  6. //d, err := json.Marshal(lp)
  7. err := lp.ToJSON(rw)
  8. if err != nil {
  9. http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)
  10. }
  11. //rw.Write(d)
  12. }

Again, use jq to test:

  1. # on the first terminal
  2. go run main.go
  3. # open another terminal
  4. curl localhost:9090 | jq
  • Get Method: modify /handlers/products.go like this: ```go func (p Products) ServeHTTP(rw http.ResponseWriter, r http.Request) { //if the method is ‘get’ if r.Method == http.MethodGet {

    1. p.getProducts(rw, r)
    2. return

    }

    //if the method is not corresponding to any methods here rw.WriteHeader(http.StatusMethodNotAllowed) }

//move old ServeHTTPcode here func (p Products) getProducts(rw http.ResponseWriter, r http.Request) { lp := data.GetProducts()

  1. err := lp.ToJSON(rw)
  2. if err != nil {
  3. http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)
  4. }

}

  1. After this modification:
  2. ```bash
  3. # these lines can get Products
  4. curl localhost:9090 | jq
  5. curl localhost:9090 -XGET | jq
  6. # these cannot get Products
  7. curl -v localhost:9090 -XDELETE | jq
  8. curl -v localhost:9090 -XPOST | jq
  • There are 5 kinds of http operations(GET, POST, PUT, PATCH, DELETE) defined.
  • So that I can apply the pattern before to set up methods for other operations:

    • /handlers/products.go ```go //… func (p Products) ServeHTTP(rw http.ResponseWriter, r http.Request) { if r.Method == http.MethodGet { p.getProducts(rw, r) return }

      if r.Method == http.MethodPost { p.addProduct(rw, r) return }

      rw.WriteHeader(http.StatusMethodNotAllowed) }

//…

func (p Products) addProduct(rw http.ResponseWriter, r http.Request) { p.l.Println(“Handle POST Product”)

  1. prod := &data.Product{}
  2. err := prod.FromJSON(r.Body)
  3. if err != nil {
  4. http.Error(rw, "Unable to marshal json", http.StatusBadRequest)
  5. }
  6. p.l.Printf("Prod: %#v", prod)

} //…

  1. - /data/products.go
  2. ```go
  3. //...
  4. //Product is JSON, Product is non-JSON list
  5. //decode Product list from JSON
  6. func (p *Product) FromJSON(r io.Reader) error {
  7. e := json.NewDecoder(r)
  8. return e.Decode(p)
  9. }
  10. //...

To test, use:

  1. # on the server terminal
  2. go run main.go
  3. # on the client terminal
  4. curl -v localhost:9090 -d '{"id": 1, "name": "tea", "description": "a nice cup of tea"}' | jq
  5. # we get on the server terminal
  6. product-api2020/04/30 11:21:58 Handle POST Product
  7. product-api2020/04/30 11:21:58 Prod: &data.Product{ID:1, Name:"tea", Description:"a nice cup of tea", Price:0, SKU:"", CreatedOn:"", UpdatedOn:"", DeletedOn:""}

Apr. 30

POST Method:

  • Continue the job of adding addProduct method:
    • 2 new functions of /data/products.go ```go //… //add a product func AddProduct(p *Product) { p.ID = getNextID() productList = append(productList, p) }

//get the next available product id func getNextID() int { lp := productList[len(productList)-1] return lp.ID + 1 } //…

  1. - modify /handlers/products.go
  2. ```go
  3. //...
  4. func (p *Products) addProducts(rw http.ResponseWriter, r *http.Request) {
  5. p.l.Println("Handle POST Product")
  6. prod := &data.Product{}
  7. err := prod.FromJSON(r.Body)
  8. if err != nil {
  9. http.Error(rw, "Unable to marshal json", http.StatusBadRequest)
  10. }
  11. data.AddProduct(prod)
  12. }
  13. //...

To test, use:

  1. # on the server terminal
  2. go run main.go
  3. # on the client terminal
  4. curl -v localhost:9090 -d '{"id": 1, "name": "tea", "description": "a nice cup of tea"}' | jq
  5. curl -v localhost:9090 | jq
  6. # on the server terminal, respond:
  7. product-api2020/04/30 19:47:26 Handle POST Product
  8. # on the client terminal, respond:
  9. % Total % Received % Xferd Average Speed Time Time Time Current
  10. Dload Upload Total Spent Left Speed
  11. 100 272 100 272 0 0 148k 0 --:--:-- --:--:-- --:--:-- 265k
  12. [
  13. {
  14. "id": 1,
  15. "name": "Latte",
  16. "description": "Frothy milky coffee",
  17. "price": 2.45,
  18. "sku": "abc323"
  19. },
  20. {
  21. "id": 2,
  22. "name": "Espresso",
  23. "description": "Short and strong coffee without milk",
  24. "price": 1.99,
  25. "sku": "fjd34"
  26. },
  27. {
  28. "id": 3,
  29. "name": "tea",
  30. "description": "a nice cup of tea",
  31. "price": 0,
  32. "sku": ""
  33. }
  34. ]

Adding a product is successful, and also the id of “tea” is 3 instead of 1 means getNextID() works.

Apr. 31 - May. 2

PUT Method:

(differences between POST and PUT:
PUT use url to pass massages, POST use extra data space to pass massages)
After GET and POST, add the PUT (update) method:

  • First we need to use PUT method to make sure the server can get the id of the product
  • /handlers/products.go:

    1. //...
    2. import (
    3. "log"
    4. "microservice/data"
    5. "net/http"
    6. "regexp" //new
    7. "strconv" //new
    8. )
    9. //...
    10. func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    11. //...
    12. if r.Method == http.MethodPut {
    13. p.l.Println("PUT", r.URL.Path)
    14. //catch "/" and digits at least 0 "0 to 9"
    15. //MustCompile means read request must follow the expression inside
    16. reg := regexp.MustCompile(`/([0-9]+)`)
    17. //returns a slice of all successive matches of the expression
    18. g := reg.FindAllStringSubmatch(r.URL.Path, -1)
    19. if len(g) != 1 {
    20. p.l.Println("Invalid URI more than one id")
    21. http.Error(rw, "Invalid URI", http.StatusBadRequest)
    22. return
    23. }
    24. if len(g[0]) != 2 {
    25. p.l.Println("Invalid URI more than one capture group")
    26. http.Error(rw, "Invalid URI", http.StatusBadRequest)
    27. return
    28. }
    29. idString := g[0][1]
    30. id, err := strconv.Atoi(idString)
    31. if err != nil {
    32. p.l.Println("Invalid URI cannot convert to number", idString)
    33. http.Error(rw, "Invalid URI", http.StatusBadRequest)
    34. return
    35. }
    36. p.l.Println("got id", id)
    37. }
    38. //...
    39. }

    To test, use: ```bash

    on the server terminal

    go run main.go

on the client terminal

curl -v localhost:9090/1 -XPUT

we get on the server terminal

product-api2020/05/02 15:34:05 PUT /1 product-api2020/05/02 15:34:05 got id 1

  1. So it can successfully get the input as an id of the product.
  2. - Then we implement actual "update" product method, in package data
  3. - /data/products.go:
  4. ```go
  5. import (
  6. "encoding/json"
  7. "fmt" //new
  8. "io"
  9. "time"
  10. )
  11. //...
  12. func UpdateProduct(id int, p *Product) error {
  13. _, pos, err := findProduct(id)
  14. if err != nil {
  15. return err
  16. }
  17. p.ID = id
  18. productList[pos] = p
  19. return nil
  20. }
  21. var ErrProductNotFound = fmt.Errorf("Product not found")
  22. func findProduct(id int) (*Product, int, error) {
  23. for i, p := range productList {
  24. if p.ID == id {
  25. return p, i, nil
  26. }
  27. }
  28. return nil, -1, ErrProductNotFound
  29. }
  30. //...
  • /handlers/products.go: ```go

//… func (p Products) updateProducts(id int, rw http.ResponseWriter, r http.Request) { p.l.Println(“Handle PUT Product”)

  1. prod := &data.Product{}
  2. err := prod.FromJSON(r.Body)
  3. if err != nil {
  4. http.Error(rw, "Unable to marshal json", http.StatusBadRequest)
  5. }
  6. err = data.UpdateProduct(id, prod)

} //… func (p Products) ServeHTTP(rw http.ResponseWriter, r http.Request) { //…

  1. if r.Method == http.MethodPut {
  2. p.l.Println("PUT", r.URL.Path)
  3. //catch "/" and digits at least 0 "0 to 9"
  4. //MustCompile means read request must follow the expression inside
  5. reg := regexp.MustCompile(`/([0-9]+)`)
  6. //returns a slice of all successive matches of the expression
  7. g := reg.FindAllStringSubmatch(r.URL.Path, -1)
  8. if len(g) != 1 {
  9. p.l.Println("Invalid URI more than one id")
  10. http.Error(rw, "Invalid URI", http.StatusBadRequest)
  11. return
  12. }
  13. if len(g[0]) != 2 {
  14. p.l.Println("Invalid URI more than one capture group")
  15. http.Error(rw, "Invalid URI", http.StatusBadRequest)
  16. return
  17. }
  18. idString := g[0][1]
  19. id, err := strconv.Atoi(idString)
  20. if err != nil {
  21. p.l.Println("Invalid URI cannot convert to number", idString)
  22. http.Error(rw, "Invalid URI", http.StatusBadRequest)
  23. return
  24. }
  25. p.updateProducts(id, rw, r)
  26. return
  27. }

//… }

  1. To test, use:
  2. ```bash
  3. # on the server terminal
  4. go run main.go
  5. # on the client terminal
  6. curl -v localhost:9090/1 -XPUT -d '{"name": "tea", "description": "a nice cup of tea"}' | jq
  7. curl localhost:9090 -XGET | jq
  8. # we get on the server terminal
  9. product-api2020/05/02 17:35:18 PUT /1
  10. product-api2020/05/02 17:35:18 Handle PUT Product
  11. # we get on the client terminal
  12. % Total % Received % Xferd Average Speed Time Time Time Current
  13. Dload Upload Total Spent Left Speed
  14. 100 184 100 184 0 0 100k 0 --:--:-- --:--:-- --:--:-- 179k
  15. [
  16. {
  17. "id": 1,
  18. "name": "tea",
  19. "description": "a nice cup of tea",
  20. "price": 0,
  21. "sku": ""
  22. },
  23. {
  24. "id": 2,
  25. "name": "Espresso",
  26. "description": "Short and strong coffee without milk",
  27. "price": 1.99,
  28. "sku": "fjd34"
  29. }
  30. ]

So that Product id 1 is updated by PUT method.

Apply Gorilla web toolkit: GET & PUT

In Summary, Gorilla web toolkit is a RESTful web toolkit, and I can convert my code into simpler version:

  • GET
  • /main.go ```go package main

import ( “context” “log” “microservice/handlers” “net/http” “os” “os/signal” “time”

  1. "github.com/gorilla/mux" //new

) //… func main(){ //create a serve mux and register handlers sm := mux.NewRouter()

  1. getRouter := sm.Methods("GET").Subrouter()
  2. getRouter.HandleFunc("/", ph.GetProducts)
  3. //sm.Handle("/", ph)

}

  1. - totally delete ServeHTTP in /handler/products.go
  2. ```go
  3. /*
  4. func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
  5. if r.Method == http.MethodGet {
  6. p.getProducts(rw, r)
  7. return
  8. }
  9. if r.Method == http.MethodPost {
  10. p.addProducts(rw, r)
  11. return
  12. }
  13. if r.Method == http.MethodPut {
  14. p.l.Println("PUT", r.URL.Path)
  15. //catch "/" and digits at least 0 "0 to 9"
  16. //MustCompile means read request must follow the expression inside
  17. reg := regexp.MustCompile(`/([0-9]+)`)
  18. //returns a slice of all successive matches of the expression
  19. g := reg.FindAllStringSubmatch(r.URL.Path, -1)
  20. if len(g) != 1 {
  21. p.l.Println("Invalid URI more than one id")
  22. http.Error(rw, "Invalid URI", http.StatusBadRequest)
  23. return
  24. }
  25. if len(g[0]) != 2 {
  26. p.l.Println("Invalid URI more than one capture group")
  27. http.Error(rw, "Invalid URI", http.StatusBadRequest)
  28. return
  29. }
  30. idString := g[0][1]
  31. id, err := strconv.Atoi(idString)
  32. if err != nil {
  33. p.l.Println("Invalid URI cannot convert to number", idString)
  34. http.Error(rw, "Invalid URI", http.StatusBadRequest)
  35. return
  36. }
  37. p.updateProducts(id, rw, r)
  38. return
  39. }
  40. rw.WriteHeader(http.StatusMethodNotAllowed)
  41. }
  42. */
  43. //change getProducts() to GetProducts() in order to let other parts can use this function
  44. func (p *Products) GetProducts(rw http.ResponseWriter, r *http.Request) {
  45. //...
  46. }

To test, use the same code in “GET Method” part, should return the same result.
At here, GET part is simplified by Gorilla.

  • Same as PUT Method
  • /main.go:

    1. func main() {
    2. //...
    3. putRouter := sm.Methods("PUT").Subrouter()
    4. putRouter.HandleFunc("/{id:[0-9]+}", ph.UpdateProducts)
    5. //...
    6. }
  • /handler/products.go ```go //capitalize function name, and modify how to pick id inside func (p Products) UpdateProducts(rw http.ResponseWriter, r http.Request) { //read variables in the request vars := mux.Vars(r) id, err := strconv.Atoi(vars[“id”]) if err != nil {

    1. http.Error(rw, "Unable to convert id", http.StatusBadRequest)
    2. return

    }

    p.l.Println(“Handle PUT Product”)

    prod := &data.Product{}

    err = prod.FromJSON(r.Body) if err != nil {

    1. http.Error(rw, "Unable to marshal json", http.StatusBadRequest)

    }

    err = data.UpdateProduct(id, prod)

}

  1. To test, use the part of PUT Method, should get the same result.
  2. <a name="YPFE4"></a>
  3. ### May. 3 - May. 4
  4. <a name="zjPU6"></a>
  5. #### Apply Gorilla web toolkit: POST
  6. - /main.go:
  7. ```go
  8. func main() {
  9. //...
  10. postRouter := sm.Methods("POST").Subrouter()
  11. postRouter.HandleFunc("/", ph.AddProduct)
  12. //...
  13. }

/handler/products.go

  1. //capitalize function name
  2. func (p *Products) AddProduct(rw http.ResponseWriter, r *http.Request) {
  3. p.l.Println("Handle POST Product")
  4. prod := &data.Product{}
  5. err := prod.FromJSON(r.Body)
  6. if err != nil {
  7. http.Error(rw, "Unable to marshal json", http.StatusBadRequest)
  8. }
  9. data.AddProduct(prod)
  10. }

May. 5

Remove duplicate codes:

  • in handlers/products.go: ```go package handlers

import ( “context” //new “log” “microservice/data” “net/http” “strconv”

  1. "github.com/gorilla/mux"

)

//…

// AddProduct - POST func (p Products) AddProduct(rw http.ResponseWriter, r http.Request) { p.l.Println(“Handle POST Product”)

  1. //remove duplicate codes, and use KeyProduct struct
  2. prod := r.Context().Value(KeyProduct{}).(data.Product)
  3. data.AddProduct(&prod)

}

// UpdateProducts - PUT func (p Products) UpdateProducts(rw http.ResponseWriter, r http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars[“id”]) if err != nil { http.Error(rw, “Unable to convert id”, http.StatusBadRequest) return }

  1. p.l.Println("Handle PUT Product", id)
  2. //remove duplicate codes, and use KeyProduct struct
  3. prod := r.Context().Value(KeyProduct{}).(data.Product)
  4. err = data.UpdateProduct(id, &prod)
  5. if err == data.ErrProductNotFound {
  6. http.Error(rw, "Product not found", http.StatusNotFound)
  7. }
  8. if err != nil {
  9. http.Error(rw, "Product not found", http.StatusInternalServerError)
  10. }

}

type KeyProduct struct{}

//send PUT and POST common parts into this function func (p Products) MiddlewareValidateProduct(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { prod := data.Product{}

  1. err := prod.FromJSON(r.Body)
  2. if err != nil {
  3. p.l.Println("[ERROR] deserializing product", err)
  4. http.Error(rw, "Error reading product", http.StatusBadRequest)
  5. return
  6. }
  7. //add product to context
  8. ctx := context.WithValue(r.Context(), KeyProduct{}, prod)
  9. req := r.WithContext(ctx)
  10. next.ServeHTTP(rw, req)
  11. })

}

  1. - Meanwhile, apply MiddlewareValidateProduct() in main.go:
  2. ```go
  3. func main() {
  4. l := log.New(os.Stdout, "product-api", log.LstdFlags)
  5. ph := handlers.NewProducts(l)
  6. //...
  7. putRouter := sm.Methods("PUT").Subrouter()
  8. putRouter.HandleFunc("/{id:[0-9]+}", ph.UpdateProducts)
  9. putRouter.Use(ph.MiddlewareValidateProduct) //new
  10. postRouter := sm.Methods("POST").Subrouter()
  11. postRouter.HandleFunc("/", ph.AddProduct)
  12. postRouter.Use(ph.MiddlewareValidateProduct) //new
  13. //...
  14. }

Apply JSON validation:

  • This part will use package “validator”:

https://github.com/go-playground/validator

Basic:
  • We mostly apply this pack to return validation errors, and we set those rules of validation in a struct:
  • data/products.go: ```go package data

import ( validator “github.com/go-playground/validator/v10” // new

  1. "encoding/json"
  2. "fmt"
  3. "io"
  4. "time"

)

type Product struct { ID int json:"id" Name string json:"name" validate:"required" //must have a name Description string json:"description" Price float32 json:"price" validate:"gt=0" //greater than zero SKU string json:"sku" CreatedOn string json:"-" UpdatedOn string json:"-" DeletedOn string json:"-" }

func (p *Product) Validate() error { validate := validator.New() return validate.Struct(p) } //…

  1. - and we create a test program called products_test.go in data/
  2. ```go
  3. package data
  4. import "testing"
  5. func TestChecksValidation(t *testing.T) {
  6. p := &Product{
  7. Name: "nics",
  8. Price: 1.00,
  9. }
  10. err := p.Validate()
  11. if err != nil {
  12. t.Fatal(err)
  13. }
  14. }

We can use the “run test” button above the function to run the test and see is the test case ok for current rules.

May. 6

Create our own validation functions:
  • data/products.go: ```go import ( “regexp” //new

    validator “github.com/go-playground/validator/v10”

    “encoding/json” “fmt” “io” “time” )

type Product struct { ID int json:"id" Name string json:"name" validate:"required" //must have a name Description string json:"description" Price float32 json:"price" validate:"gt=0" //greater than zero SKU string json:"sku" validate:"required,sku" //apply validation “sku” CreatedOn string json:"-" UpdatedOn string json:"-" DeletedOn string json:"-" }

func (p *Product) Validate() error { validate := validator.New() validate.RegisterValidation(“sku”, validateSKU)

  1. return validate.Struct(p)

}

//validate the vairable “SKU” in the Product func validateSKU(fl validator.FieldLevel) bool { re := regexp.MustCompile([a-z]+-[a-z]+-[a-z]+) matches := re.FindAllString(fl.Field().String(), -1)

  1. if len(matches) != 1 {
  2. return false
  3. }
  4. return true

} //…

  1. <a name="FXrHL"></a>
  2. #### Complete middleware:
  3. ```go
  4. //...
  5. type KeyProduct struct{}
  6. func (p Products) MiddlewareValidateProduct(next http.Handler) http.Handler {
  7. return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  8. prod := data.Product{}
  9. err := prod.FromJSON(r.Body)
  10. if err != nil {
  11. p.l.Println("[ERROR] deserializing product", err)
  12. http.Error(rw, "Error reading product", http.StatusBadRequest)
  13. return
  14. }
  15. //validate the product
  16. err = prod.Validate()
  17. if err != nil {
  18. p.l.Println("[ERROR] validating product", err)
  19. http.Error(
  20. rw,
  21. fmt.Sprintf("Error validating product: %s", err),
  22. http.StatusBadRequest,
  23. )
  24. return
  25. }
  26. //add product to context
  27. ctx := context.WithValue(r.Context(), KeyProduct{}, prod)
  28. req := r.WithContext(ctx)
  29. next.ServeHTTP(rw, req)
  30. })
  31. }

Then we would finally get a validator with enough checking methods.
To test:

  1. # client
  2. curl localhost:9090 -d '{"id": 1, "name": "tea", "price": 1.23, "description": "a nice cup of tea"}' | jq
  3. # server
  4. product-api2020/05/06 17:11:59 Handle POST Product
  5. product-api2020/05/06 17:12:06 [ERROR] validating product Key: 'Product.SKU' Error:Field validation for 'SKU' failed on the 'required' tag
  6. # client
  7. curl localhost:9090 -d '{"id": 1, "name": "tea", "description": "a nice cup of tea", "SKU": "abc-def-ghi"}' | jq
  8. # server
  9. product-api2020/05/06 17:13:40 [ERROR] validating product Key: 'Product.Price' Error:Field validation for 'Price' failed on the 'gt' tag

so it successfully rejects invalid requests.

May. 11

  • About “go Swagger”:
    • Swagger is a framework that can let users better write API files
    • go Swagger is the golang version of Swagger
    • homepage of swagger: https://goswagger.io/

//Cause cannot successfully install go-swagger package, and swagger is too far away from the current purpose of golang and microservice learning, jump to later classes.
//The most important part of microservice is finished, so from now on get into the learnimg of gin HTTP framework.