- Requirement of environment:
- Apr. 22
- on the first terminal
- open another terminal
- -v is for details, to see the response more clearly this time, ignore it
- on the first terminal
- open another terminal
- Apr. 29
- Apr. 30
- Apr. 31 - May. 2
- on the server terminal
- on the client terminal
- we get on the server terminal
- May. 5
- May. 6
- May. 11
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”) })
//":9090" means listen to any port 9090 from any nodeshttp.ListenAndServe(":9090", nil)
}
To test:```bash# on the first terminalgo run main.go# open another terminalcurl -v localhost:9090
Then you will see the first terminal prints “Hello World”.
- Extend path above by changing the path, add:
To test: ```go //on the first terminal go run main.gohttp.HandleFunc("/goodbye", func(http.ResponseWriter, *http.Request) {log.Println("Goodbye World")})
//open another terminal curl -v localhost:9090/goodbye
Then you will see the first terminal prints "Goodbye World".---<a name="dQdxL"></a>### Apr. 23- Extract data- Import "io/ioutil" && ioutil.ReadAll((*http.Request).Body)- example:```gopackage mainimport ("io/ioutil""log""net/http")func main() {http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {log.Println("Hello World")d, _ := ioutil.ReadAll(r.Body)//fmt is on the serverlog.Printf("Data %s\n", d)})//":9090" means listen to any port 9090 from any nodeshttp.ListenAndServe(":9090", nil)}
To test:
//on the first terminalgo run main.go//open another terminalcurl -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”)
d, _ := ioutil.ReadAll(r.Body)//fmt is server to userfmt.Fprintf(rw, "Hello %s\n", d)})//":9090" means listen to any port 9090 from any nodeshttp.ListenAndServe(":9090", nil)
}
To test:```bash# on the first terminalgo run main.go# open another terminal# -v is for details, to see the response more clearly this time, ignore itcurl -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 }
<a name="avEPH"></a>### Apr. 25- Made a package as handlers/hello.go```gopackage handlersimport ("fmt""io/ioutil""log""net/http")type Hello struct {l *log.Logger}func NewHello(l *log.Logger) *Hello {return &Hello{l}}//ServeHTTP is mandatoryfunc (h *Hello) ServeHTTP(rw http.ResponseWriter, r *http.Request) {h.l.Println("Hello World")d, err := ioutil.ReadAll(r.Body)if err != nil {http.Error(rw, "Oops", http.StatusBadRequest)return}fmt.Fprintf(rw, "Hello %s\n", d)}
- And use it in main.go before ```go package main
import ( “log” “net/http” “os”
"microservice/handlers"
)
func main() { l := log.New(os.Stdout, “product-api”, log.LstdFlags) hh := handlers.NewHello(l)
sm := http.NewServeMux()sm.Handle("/", hh)http.ListenAndServe(":9090", sm)
}
(Doesn't work, back to set up go.mod to make it as a project)<a name="gDgS2"></a>### Apr. 26- initialized go.mod in microservice/```bashgo mod init microservice
- now I can apply package handlers.
open another terminal
-v is for details, to see the response more clearly this time, ignore it
curl -d ‘Nic’ localhost:9090
Server prints "Hello World", and responses "Hello Nic".- handle '/goodbye', set up goodbye.go in handlers folder:```gopackage handlersimport ("log""net/http")type Goodbye struct {l *log.Logger}func NewGoodbye(l *log.Logger) *Goodbye {return &Goodbye{l}}//ServeHTTP is mandatoryfunc (g *Goodbye) ServeHTTP(rw http.ResponseWriter, r *http.Request) {rw.Write([]byte("Byeee"))}
- Then update main.go: ```go package main
import ( “log” “net/http” “os”
"microservice/handlers"
)
func main() { l := log.New(os.Stdout, “product-api”, log.LstdFlags) hh := handlers.NewHello(l) gh := handlers.NewGoodbye(l)
//integrate handlerssm := http.NewServeMux()sm.Handle("/", hh)sm.Handle("/goodbye", gh)http.ListenAndServe(":9090", sm)
}
- To test:```bash# on the first terminalgo run main.go# open another terminalcurl -v localhost:9090/goodbye
Server responses “Byeee”.
Apr. 27
- Write a custom server: ```go package main
import ( “log” “net/http” “os” “time”
"microservice/handlers"
)
func main() { l := log.New(os.Stdout, “product-api”, log.LstdFlags) hh := handlers.NewHello(l) gh := handlers.NewGoodbye(l)
sm := http.NewServeMux()sm.Handle("/", hh)sm.Handle("/goodbye", gh)//create a custom servers := &http.Server{Addr: ":9090",Handler: sm,ReadTimeout: 120 * time.Second,IdleTimeout: 1 * time.Second,WriteTimeout: 1 * time.Second,}s.ListenAndServe()
}
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.- notice of interrupt/kill and set graceful shutdown:```gopackage mainimport ("context""log""net/http""os""os/signal""time""microservice/handlers")func main() {l := log.New(os.Stdout, "product-api", log.LstdFlags)hh := handlers.NewHello(l)gh := handlers.NewGoodbye(l)sm := http.NewServeMux()sm.Handle("/", hh)sm.Handle("/goodbye", gh)//create a custom servers := &http.Server{Addr: ":9090",Handler: sm,ReadTimeout: 120 * time.Second,IdleTimeout: 1 * time.Second,WriteTimeout: 1 * time.Second,}//go routine - spread an extra branch as the independent servergo func() {err := s.ListenAndServe()if err != nil {l.Fatal(err)}}()//print the signal of interruptsigChan := make(chan os.Signal)signal.Notify(sigChan, os.Interrupt)signal.Notify(sigChan, os.Kill)sig := <-sigChanl.Println("Received terminate, graceful shutdown", sig)//shutdown after everything is finished and wait for 30 more secondstc, _ := context.WithTimeout(context.Background(), 30*time.Second)s.Shutdown(tc)}
To test:
# on server side terminalgo run main.go^C
Shows:
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(), }, }
- Then in folder "handler", also setup a product.go:```gopackage handlersimport ("encoding/json""log""microservice/data""net/http")type Products struct {l *log.Logger}func NewProducts(l *log.Logger) *Products {return &Products{l}}func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {lp := data.GetProducts()//read data as jsond, err := json.Marshal(lp)if err != nil {http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)}rw.Write(d)}
Finally, modify main.go for Products:
//...ph := handlers.NewProducts(l)sm := http.NewServeMux()sm.Handle("/", ph)//...
To test: ```bash
on the first terminal
go run main.go
open another terminal
curl localhost:9090
The output looks like:```bash[{"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:
yum install jq# on the first terminalgo run main.go# open another terminalcurl localhost:9090 | jq
This time, the output looks way better:
% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed100 442 100 442 0 0 371k 0 --:--:-- --:--:-- --:--:-- 431k[{"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": ""}]
Add struct tags/annotations for elements:
//...type Product struct {ID int `json:"id"`Name string `json:"name"`Description string `json:"description"`Price float32 `json:"price"`SKU string `json:"sku"`CreatedOn string `json:"-"`UpdatedOn string `json:"-"`DeletedOn string `json:"-"`}//...
This way affects what jq shows, we test in the same way and get:
# on the first terminalgo run main.go# open another terminalcurl localhost:9090 | jq% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed100 196 100 196 0 0 31106 0 --:--:-- --:--:-- --:--:-- 32666[{"id": 1,"name": "Latte","description": "Frothy milky coffee","price": 2.45,"sku": "abc323"},{"id": 2,"name": "Espresso","description": "Short and strong coffee without milk","price": 1.99,"sku": "fjd34"}]
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 } //…
This step defines a type to simplify []*Product as Products, and allows converting Products into JSON for some methods.- Modify /handlers/product.go to apply ToJSON above:```gofunc (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {lp := data.GetProducts()//d, err := json.Marshal(lp)err := lp.ToJSON(rw)if err != nil {http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)}//rw.Write(d)}
Again, use jq to test:
# on the first terminalgo run main.go# open another terminalcurl 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 {
p.getProducts(rw, r)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()
err := lp.ToJSON(rw)if err != nil {http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)}
}
After this modification:```bash# these lines can get Productscurl localhost:9090 | jqcurl localhost:9090 -XGET | jq# these cannot get Productscurl -v localhost:9090 -XDELETE | jqcurl -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”)
prod := &data.Product{}err := prod.FromJSON(r.Body)if err != nil {http.Error(rw, "Unable to marshal json", http.StatusBadRequest)}p.l.Printf("Prod: %#v", prod)
} //…
- /data/products.go```go//...//Product is JSON, Product is non-JSON list//decode Product list from JSONfunc (p *Product) FromJSON(r io.Reader) error {e := json.NewDecoder(r)return e.Decode(p)}//...
To test, use:
# on the server terminalgo run main.go# on the client terminalcurl -v localhost:9090 -d '{"id": 1, "name": "tea", "description": "a nice cup of tea"}' | jq# we get on the server terminalproduct-api2020/04/30 11:21:58 Handle POST Productproduct-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 } //…
- modify /handlers/products.go```go//...func (p *Products) addProducts(rw http.ResponseWriter, r *http.Request) {p.l.Println("Handle POST Product")prod := &data.Product{}err := prod.FromJSON(r.Body)if err != nil {http.Error(rw, "Unable to marshal json", http.StatusBadRequest)}data.AddProduct(prod)}//...
To test, use:
# on the server terminalgo run main.go# on the client terminalcurl -v localhost:9090 -d '{"id": 1, "name": "tea", "description": "a nice cup of tea"}' | jqcurl -v localhost:9090 | jq# on the server terminal, respond:product-api2020/04/30 19:47:26 Handle POST Product# on the client terminal, respond:% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed100 272 100 272 0 0 148k 0 --:--:-- --:--:-- --:--:-- 265k[{"id": 1,"name": "Latte","description": "Frothy milky coffee","price": 2.45,"sku": "abc323"},{"id": 2,"name": "Espresso","description": "Short and strong coffee without milk","price": 1.99,"sku": "fjd34"},{"id": 3,"name": "tea","description": "a nice cup of tea","price": 0,"sku": ""}]
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:
//...import ("log""microservice/data""net/http""regexp" //new"strconv" //new)//...func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {//...if r.Method == http.MethodPut {p.l.Println("PUT", r.URL.Path)//catch "/" and digits at least 0 "0 to 9"//MustCompile means read request must follow the expression insidereg := regexp.MustCompile(`/([0-9]+)`)//returns a slice of all successive matches of the expressiong := reg.FindAllStringSubmatch(r.URL.Path, -1)if len(g) != 1 {p.l.Println("Invalid URI more than one id")http.Error(rw, "Invalid URI", http.StatusBadRequest)return}if len(g[0]) != 2 {p.l.Println("Invalid URI more than one capture group")http.Error(rw, "Invalid URI", http.StatusBadRequest)return}idString := g[0][1]id, err := strconv.Atoi(idString)if err != nil {p.l.Println("Invalid URI cannot convert to number", idString)http.Error(rw, "Invalid URI", http.StatusBadRequest)return}p.l.Println("got id", id)}//...}
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
So it can successfully get the input as an id of the product.- Then we implement actual "update" product method, in package data- /data/products.go:```goimport ("encoding/json""fmt" //new"io""time")//...func UpdateProduct(id int, p *Product) error {_, pos, err := findProduct(id)if err != nil {return err}p.ID = idproductList[pos] = preturn nil}var ErrProductNotFound = fmt.Errorf("Product not found")func findProduct(id int) (*Product, int, error) {for i, p := range productList {if p.ID == id {return p, i, nil}}return nil, -1, ErrProductNotFound}//...
- /handlers/products.go: ```go
//… func (p Products) updateProducts(id int, rw http.ResponseWriter, r http.Request) { p.l.Println(“Handle PUT Product”)
prod := &data.Product{}err := prod.FromJSON(r.Body)if err != nil {http.Error(rw, "Unable to marshal json", http.StatusBadRequest)}err = data.UpdateProduct(id, prod)
} //… func (p Products) ServeHTTP(rw http.ResponseWriter, r http.Request) { //…
if r.Method == http.MethodPut {p.l.Println("PUT", r.URL.Path)//catch "/" and digits at least 0 "0 to 9"//MustCompile means read request must follow the expression insidereg := regexp.MustCompile(`/([0-9]+)`)//returns a slice of all successive matches of the expressiong := reg.FindAllStringSubmatch(r.URL.Path, -1)if len(g) != 1 {p.l.Println("Invalid URI more than one id")http.Error(rw, "Invalid URI", http.StatusBadRequest)return}if len(g[0]) != 2 {p.l.Println("Invalid URI more than one capture group")http.Error(rw, "Invalid URI", http.StatusBadRequest)return}idString := g[0][1]id, err := strconv.Atoi(idString)if err != nil {p.l.Println("Invalid URI cannot convert to number", idString)http.Error(rw, "Invalid URI", http.StatusBadRequest)return}p.updateProducts(id, rw, r)return}
//… }
To test, use:```bash# on the server terminalgo run main.go# on the client terminalcurl -v localhost:9090/1 -XPUT -d '{"name": "tea", "description": "a nice cup of tea"}' | jqcurl localhost:9090 -XGET | jq# we get on the server terminalproduct-api2020/05/02 17:35:18 PUT /1product-api2020/05/02 17:35:18 Handle PUT Product# we get on the client terminal% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed100 184 100 184 0 0 100k 0 --:--:-- --:--:-- --:--:-- 179k[{"id": 1,"name": "tea","description": "a nice cup of tea","price": 0,"sku": ""},{"id": 2,"name": "Espresso","description": "Short and strong coffee without milk","price": 1.99,"sku": "fjd34"}]
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”
"github.com/gorilla/mux" //new
) //… func main(){ //create a serve mux and register handlers sm := mux.NewRouter()
getRouter := sm.Methods("GET").Subrouter()getRouter.HandleFunc("/", ph.GetProducts)//sm.Handle("/", ph)
}
- totally delete ServeHTTP in /handler/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.addProducts(rw, r)return}if r.Method == http.MethodPut {p.l.Println("PUT", r.URL.Path)//catch "/" and digits at least 0 "0 to 9"//MustCompile means read request must follow the expression insidereg := regexp.MustCompile(`/([0-9]+)`)//returns a slice of all successive matches of the expressiong := reg.FindAllStringSubmatch(r.URL.Path, -1)if len(g) != 1 {p.l.Println("Invalid URI more than one id")http.Error(rw, "Invalid URI", http.StatusBadRequest)return}if len(g[0]) != 2 {p.l.Println("Invalid URI more than one capture group")http.Error(rw, "Invalid URI", http.StatusBadRequest)return}idString := g[0][1]id, err := strconv.Atoi(idString)if err != nil {p.l.Println("Invalid URI cannot convert to number", idString)http.Error(rw, "Invalid URI", http.StatusBadRequest)return}p.updateProducts(id, rw, r)return}rw.WriteHeader(http.StatusMethodNotAllowed)}*///change getProducts() to GetProducts() in order to let other parts can use this functionfunc (p *Products) GetProducts(rw http.ResponseWriter, r *http.Request) {//...}
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:
func main() {//...putRouter := sm.Methods("PUT").Subrouter()putRouter.HandleFunc("/{id:[0-9]+}", ph.UpdateProducts)//...}
/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 {
http.Error(rw, "Unable to convert id", http.StatusBadRequest)return
}
p.l.Println(“Handle PUT Product”)
prod := &data.Product{}
err = prod.FromJSON(r.Body) if err != nil {
http.Error(rw, "Unable to marshal json", http.StatusBadRequest)
}
err = data.UpdateProduct(id, prod)
}
To test, use the part of PUT Method, should get the same result.<a name="YPFE4"></a>### May. 3 - May. 4<a name="zjPU6"></a>#### Apply Gorilla web toolkit: POST- /main.go:```gofunc main() {//...postRouter := sm.Methods("POST").Subrouter()postRouter.HandleFunc("/", ph.AddProduct)//...}
/handler/products.go
//capitalize function namefunc (p *Products) AddProduct(rw http.ResponseWriter, r *http.Request) {p.l.Println("Handle POST Product")prod := &data.Product{}err := prod.FromJSON(r.Body)if err != nil {http.Error(rw, "Unable to marshal json", http.StatusBadRequest)}data.AddProduct(prod)}
May. 5
Remove duplicate codes:
- in handlers/products.go: ```go package handlers
import ( “context” //new “log” “microservice/data” “net/http” “strconv”
"github.com/gorilla/mux"
)
//…
// AddProduct - POST func (p Products) AddProduct(rw http.ResponseWriter, r http.Request) { p.l.Println(“Handle POST Product”)
//remove duplicate codes, and use KeyProduct structprod := r.Context().Value(KeyProduct{}).(data.Product)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 }
p.l.Println("Handle PUT Product", id)//remove duplicate codes, and use KeyProduct structprod := r.Context().Value(KeyProduct{}).(data.Product)err = data.UpdateProduct(id, &prod)if err == data.ErrProductNotFound {http.Error(rw, "Product not found", http.StatusNotFound)}if err != nil {http.Error(rw, "Product not found", http.StatusInternalServerError)}
}
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{}
err := prod.FromJSON(r.Body)if err != nil {p.l.Println("[ERROR] deserializing product", err)http.Error(rw, "Error reading product", http.StatusBadRequest)return}//add product to contextctx := context.WithValue(r.Context(), KeyProduct{}, prod)req := r.WithContext(ctx)next.ServeHTTP(rw, req)})
}
- Meanwhile, apply MiddlewareValidateProduct() in main.go:```gofunc main() {l := log.New(os.Stdout, "product-api", log.LstdFlags)ph := handlers.NewProducts(l)//...putRouter := sm.Methods("PUT").Subrouter()putRouter.HandleFunc("/{id:[0-9]+}", ph.UpdateProducts)putRouter.Use(ph.MiddlewareValidateProduct) //newpostRouter := sm.Methods("POST").Subrouter()postRouter.HandleFunc("/", ph.AddProduct)postRouter.Use(ph.MiddlewareValidateProduct) //new//...}
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
"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"
CreatedOn string json:"-"
UpdatedOn string json:"-"
DeletedOn string json:"-"
}
func (p *Product) Validate() error { validate := validator.New() return validate.Struct(p) } //…
- and we create a test program called products_test.go in data/```gopackage dataimport "testing"func TestChecksValidation(t *testing.T) {p := &Product{Name: "nics",Price: 1.00,}err := p.Validate()if err != nil {t.Fatal(err)}}
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)
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)
if len(matches) != 1 {return false}return true
} //…
<a name="FXrHL"></a>#### Complete middleware:```go//...type KeyProduct struct{}func (p Products) MiddlewareValidateProduct(next http.Handler) http.Handler {return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {prod := data.Product{}err := prod.FromJSON(r.Body)if err != nil {p.l.Println("[ERROR] deserializing product", err)http.Error(rw, "Error reading product", http.StatusBadRequest)return}//validate the producterr = prod.Validate()if err != nil {p.l.Println("[ERROR] validating product", err)http.Error(rw,fmt.Sprintf("Error validating product: %s", err),http.StatusBadRequest,)return}//add product to contextctx := context.WithValue(r.Context(), KeyProduct{}, prod)req := r.WithContext(ctx)next.ServeHTTP(rw, req)})}
Then we would finally get a validator with enough checking methods.
To test:
# clientcurl localhost:9090 -d '{"id": 1, "name": "tea", "price": 1.23, "description": "a nice cup of tea"}' | jq# serverproduct-api2020/05/06 17:11:59 Handle POST Productproduct-api2020/05/06 17:12:06 [ERROR] validating product Key: 'Product.SKU' Error:Field validation for 'SKU' failed on the 'required' tag# clientcurl localhost:9090 -d '{"id": 1, "name": "tea", "description": "a nice cup of tea", "SKU": "abc-def-ghi"}' | jq# serverproduct-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.
