สำหรับ Week นี้ เป็นบทที่เยอะมาก 555 รอบหน้าแบ่งสอน 2 Week จะดีมาก ส่วนตัวแยก 2 Blog เหมือนกัน
1. REST API
- Vocab
Components of a URL
HTTP verbs GET (ดึงข้อมูล Select) / POST (Create) / PUT (Update) / DELETE
Resources พวก Path ที่เราอ้า่งอิงกัน เช่น /orders บราๆ ส่วนใหญ่ผมเรียก EndPoints 555
- RESTful Resource Naming Conventions
เวลาไปทำงานกันคนอื่น พอจะเดา patterns และมี standard
- Pluralized URIs + URIs as nounsเช่น users , orders, funds
Should NOT: /getUser Should: /users/{id}
- Query parameters ถ้าจำเป็น เช่น กำหนดการ Sort เป็นต้น
ฟังแล้วแอบงง หลาย Query param แล้วมัน And / Or นะ จำได้ว่าขึ้นกับ Controller ที่รับมา แล้วเขียนตีความเอง
การ Sort /users?sort=age การ status /tickets?status=done
- Forward slashes for hierarchy
เช่น /users/{id}/orders >> บอกว่า user มี order อะไรบ้าง /portfolios/{id}/holdings >> บอกว่า Portfolio มีหลักทรัพย์ อะไรบ้าง มองอีกมุม /users/1/orders เท่ากับ /orders?userID=u1
- Do not use file extensions ให้ใช้ Content-Type header ส่วนให้ pattern นี้
น่ามีจะเรื่องเดียวที่ยกเว้น sitemap / rss feed มั้ง - Lowercase letters and dashes - จริงมันแสดงได้ แต่ url มันจะไม่สวย จะมี % มาด้วย เช่น space จะเป็น %20
อันนี้ผมเพิ่งรู้ บางก็พลาด ตัว _ 555 Should NOT: /users/{id}/pending_orders Should:/users/{id}/pending-orders
และก็ Coding เยอะขึ้น เห็นใน Discord มีแชร์ Coding Style Guide เลยเอามาแปะด้วย guide/style.md at master · uber-go/guide (github.com)
2. net/http
Lib "net/http" ที่มากับ Golang ที่ช่วยให้ขึ้น Server ได้ง่ายๆ
- sample code กำหนด handle
http.HandleFunc("<<Path>>, <<handler>>)
- Code ทั้งหมด กำหนด path + handler เรียบร้อยแล้ว start server http.ListenAndServe(":10170", nil)
package main import ( "log" "net/http" ) func handleAboutMe(w http.ResponseWriter, r *http.Request) { ` raw string w.Write([]byte(`{name: "PingkungA", age: 33}`)) } func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, World!")) }) //HandleFunc เขียนได้ 2 แบบ //1. ทำ func แยก โดยรับ (w http.ResponseWriter, r *http.Request) http.HandleFunc("/aboutme", handleAboutMe) //2. เขียน func เลย http.HandleFunc("/aboutme1", func(w http.ResponseWriter, r *http.Request) { //Filter Specfic Http Method if r.Method != "GET" { http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) return } w.Write([]byte(`{name: "PingkungA_GET", age: 33}`)) }) log.Println("Starting server on :10170") //ListenAndServ - Start Server log.Fatal(http.ListenAndServe(":10170", nil)) log.Println("Server Stopped") }
- ดัก request method จาก req *http.Request
req *http.Request - detect method if req.Method ="POST" { //you logic }
json.Marshal() //แปลง Struct > ByteArray Marshal=จัดเรียง เมื่อก่อนเคยลองหาเล่น เจอแปลๆไทยว่าจอมพล คงระดม CPU ไปทำของให้มั้ง 555 Unmarshall - Pointer & - Struct public ตัวใหญ่ - String > JSON มันไม่สนเล็กใหญ่ ดู key เขียนตรง > tag > field_name
3. Middleware
Middleware เป็นตัวกลางที่มาช่วยเพิ่มความสามารถต่างๆ เช่น Logging ให้กับ Object ที่สนใจ Request / Response มี pattern
First Class Function - ตัว Function ที่ assigned to variables, passed as arguments to other functions and returned from other functions
- assigned to variables
SampleFunc := Sum //มันมองเป็น Value result := SampleFunc(1, 2) //========================== func sum(a int, b int) int { return a + b }
- passed as arguments to other functions a func(a, b int) int
package main import ( "fmt" ) func Math(a func(a, b int) int) { fmt.Println(a(25, 33)) } func main() { f := func(a, b int) int { return a + b } Math(f) }
package main import ( "fmt" ) func Math() func(a, b int) int { f := func(a, b int) int { return a + b } return f } func main() { returnFunc := Math() fmt.Println(returnFunc(25, 33)) }
Function Literals - Function ที่ไม่มีชื่อ เป็น Anonymous / Closure
package main import "fmt" func square(x int) func() int { return func() int { //anonymous function x = x * x return x } } func main() { result:= square(2) fmt.Println("2 squared is: ", result) //4 }
Higher-Order Function เป็น Func ที่่อาจจะที่รับ Func มาเป็น Input หรือ คืน Func ออกเป็น Output หรือ ทำทั้งสองอย่าง
takes one or more functions as arguments and/or returns a function as its result
กลับที่มาที่ Middleware เจ้าตัว Middleware เป็นตัวกลางที่มาช่วยเพิ่มความสามารถต่างๆ เช่น Logging ให้กับ Object ที่สนใจ Request / Response ทำ wrapper นั้นเอง โดยมี pattern ที่ใช้ [Design Pattern] Decorator Pattern นั่นเอง (มี Blog ด้วย) //เข้าใจและว่าทำไมถึงสอน First Class Function / Higher-Order Function
Receiver functions
Mux (http multiplexer) ใช้ Middleware หลายๆตัวพร้อมกัน
func main() { mux := http.NewServeMux() mux.HandleFunc("/users", userHandler) mux.HandleFunc("/healthcheck", healthCheckHandler) //Inject Log On ServeHTTP Method logMux := Logger{Handler: mux} srv := http.Server{ Addr: ":10170", Handler: logMux, } log.Fatal(srv.ListenAndServe()) }
ไฟล์ learn-go/API/2Middleware/2mux/json_http_mux.go at main · pingkunga/learn-go (github.com)
นอกจาก Log แล้ว อีกตัวอย่างที่เอา Middleware มาใช้งาน จะมีแต่ตัว Basic Authen
- Basic Auth
ปกติ Go มีพวกนี้มาให้แล้ว โดยถ้า Implement ครบ ตอนลองเข้า Endpoint จะมี Popup ถาม
func userHandler(w http.ResponseWriter, r *http.Request) { usr, pwd, ok := r.BasicAuth() log.Println("auth:", usr, pwd, ok) //OK กรอกมาครบไหม if !ok { w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message": "No basic auth present"}`)) return } //ตรวจสิทธิ if !isAuthorised(usr, pwd) { w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message": "Invalid username or password"}`)) return } if r.Method == "GET" { //Your Logic } if r.Method == "POST" { //Your Logic } }
ไฟล์ BasicAuth: learn-go/API/2Middleware/3auth/json_http_mux_auth.go at main · pingkunga/learn-go (github.com)
- Middleware Auth
จากแบบ Basic Auth ถ้าจะเอาเรื่อง Auth ไปยัดใส่ มันต้องไปแปะลงใน Handler ทุกอันเลย เลยเป็นที่มาของการใช้ middleware
- middleware - เอา Logic Auth มายัด ถ้าผ่านค่อยไปทำในส่วนของ next ในที่นี้ endpoint users
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { usr, pwd, ok := req.BasicAuth() log.Println("auth:", usr, pwd, ok) if !ok { w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message": "No basic auth present"}`)) return } if !isAuthorised(usr, pwd) { w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message": "Invalid username or password"}`)) return } //Pass to End Point fmt.Println("Auth Success") next(w, req) return } }
- เอา middleware หุ้ม
func main() { mux := http.NewServeMux() //Add AuthMiddleware ถ้าผ่านค่อยทำ userHandler mux.HandleFunc("/users", AuthMiddleware(userHandler)) mux.HandleFunc("/healthcheck", healthCheckHandler) logMux := Logger{Handler: mux} srv := http.Server{ Addr: ":10170", Handler: logMux, } log.Fatal(srv.ListenAndServe()) }
ไฟล์ Middleware Auth: learn-go/API/2Middleware/3auth/json_http_mux_authMiddleware.go at main · pingkunga/learn-go (github.com)
อ๋อ พวก dotnet / spring มี middleware นะ ถ้า spring พวก Filter นับเหมือนกัน How to Define a Spring Boot Filter? | Baeldung //ไล่ code พอดีเลยมาแปะไว้
4. Echo HTTP API Framework
จากเดิมที่ใช้ net/http ที่มันจะเขียน Code เองระดับนึง แล้วมี พวก middleware มาให้ ทำอะไรแนวๆ minimal api แบบ dotnet ได้
- ตัว Echo มี middleware บางอันให้แล้ว
- มี Context มาจัดการ httprequest / httpresponsewriter มาให้แล้ว ใน handler จะลดพวก if ดัก
//Setup go mod init github.com/<user>/<repo_name> go mod tidy go get github.com/labstack/echo/v4 go get github.com/labstack/echo/v4/middleware
- Map Code แบบ Plain vs Framework
//Old Plain func userHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { bj, errj := json.Marshal(users) if errj != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Server Error")) w.Write([]byte(errj.Error())) return } w.Write(bj) return } if r.Method == "POST" { //log.Println("POST") --->> <Move to Log Middleware> body, err := io.ReadAll(r.Body) if err != nil { fmt.Fprintf(w, "error : %v", err) return } u := User{} err = json.Unmarshal(body, &u) if err != nil { fmt.Fprintf(w, "error: %v", err) return } users = append(users, u) fmt.Printf("% #v\n", users) fmt.Fprintf(w, "hello %s created users", "POST") return } }
//New with ECHO func getUserHandler(c echo.Context) error { /* Old Manual Logic if r.Method == "GET" { //log.Println("GET") --->> <Move to Log Middleware> bj, errj := json.Marshal(users) if errj != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Server Error")) w.Write([]byte(errj.Error())) return } w.Write(bj) return } */ return c.JSON(http.StatusOK, users) } type Err struct { Message string `json:"message"` } func createUserHandler(c echo.Context) error { usr := User{} if err := c.Bind(&usr); err != nil { //return err //Format error message return c.JSON(http.StatusBadRequest, Err{Message: err.Error()}) } users = append(users, usr) fmt.Println("Created user:", usr) return c.JSON(http.StatusCreated, usr) }
สำหรับพวก Middleware - Log / BasicAuth (ใช้ Group แยก ตรวจ / ไม่ตรวจ path ไหน) ดูได้จากไฟล์ server.go
5. SQL Database
- ElephantSQL
elephantsql - PostgreSQL as a Service //ดีอ่า plan free ไม่ต้อง Add บัตร
- Create Instance เลือก Free Plan > เลือก Region > Review > Create Instance ฃ
- เข้าไป Instance Copy url ตัว App ใช้
ถ้า local ใช้ docker compose ได้ ปกติผมใช้ท่านี้ เพิ่งมารู้ว่ามี Service แนวๆนี้นอกจาก Cloud เจ้าใหญ่ๆ
ส่วน url หรือ DB Connection String จะยัดลง ENV คำสั่งประมาณนี้
//ENV //Windows Powershell $env:DB_HOST='postgres://xomspvtd:x8g_LeOMU_Sb0QO8o9at3vK8n116hHTe@tiny.db.elephantsql.com/xomspvtd' echo $env:DB_HOST //Linux DB_HOST='<<Your Conn String>>' echo $DB_HOST
- Setup
//Setup go mod init github.com/<user>/<repo_name> go mod tidy //Postgres SQL Driver ใข้คู่กับ Basic Interface "database/sql" go get github.com/lib/pq
func init() - ทำงานคล้ายกับ Constructor
import ( "database/sql" "log" _ "github.com/lib/pq" ) //มี _ เพราะต้องเรียก func init() เพื่อไป set ค่าตาม "database/sql"
สำหรับ Code ตามนี้
- CRUD: learn-go/API/4APIWithDB/1SQL at main · pingkunga/learn-go (github.com)
- API with DB: learn-go/API/4APIWithDB/2API at main · pingkunga/learn-go (github.com)
เห็นมัน SQL เพียวๆ มันมี ORM ไหม แบบทาง C# มี EF / Dapper ส่วน Java จะเป็นตัว Hibernate ลองไปหาดูเหมือนมีนะ
- Lib: GORM - The fantastic ORM library for Golang, aims to be developer friendly.
- Benchmark: efectn/go-orm-benchmarks: Advanced benchmarks for +15 Go ORMs. (github.com)
6. API Integration Test
ปกติแล้ว go สนใจ _test แต่ตั้งชื่อให้มันสื่อด้วยว่าเป็น test รูปแบบไหน เช่น unit test (xxx__test) / Integration Test (xxx_integration_test)
- Driver คนที่ส่ง request เข้ามา ตัว Test เรานี่เอง ลองใช้ stretchr/testify: A toolkit with common assertions and mocks that plays nicely with the standard library (github.com) เริ่ม Assert เหมือนภาษาอื่นๆและ ตอนใช้ใส่
go get github.com/stretchr/testify import "github.com/stretchr/testify/assert"
- ที่เหลือเขียน Test ปกติ
- สุดท้ายติด Tag ด้วยตอน Run Test Local / CI จะได้
แปะหัวไฟล์ //go:build integration ตอน Run go test -v -tags=integration
ถ้าไม่ต้องการต่อ DB ต้องทำให้เล็กลง นั้นทำ unit test ตัด Dependency ออกทำ Stub ของ Func ที่ต่อ DB ให้คืนของหลอกมาๆ
7. Graceful shutdown
Graceful shutdown - ตายแบบหนังจีน ฝากลูกเมียข้าด้วย แล้วทำ request อยู่เดิมให้หมด
Goroutine - ทำให้งานเป็น Concurrent ซ่อนความซับซ้อนของ Thread อีกที มันทำงานแตกคนละ process จากตัว main เลย
go <your function>
Channels - ทำให้ 2 process มีความสัมพันธ์กัน สามารถสื่อสารกันได้ เพราะประเด็นนึงจากการใข้ Goroutine มันไปทำงานข้างหลังเงียบๆ ถ้าไม่หน่วงเวลารอ หรือมี Blocking Process (มีอีก Task ทำงานคู่กัน)
สำหรับ Goroutine + Channels มันคล้ายๆกับ Task ใน C# แตก ทำขนานกัน ต้องรออะไร เป็นต้น
เหมือน Channel จะลึกว่าตรงจับจาก os ได้นะ เช่น os.Signal อย่างพวก os.Interrupt, syscall.SIGTERM, syscall.SIGINT
และเราเอาเจ้า Signal จาก OS มาทำ Graceful shutdown แต่ต้องมี Pattern การเขียนนะ เช่น แยก Task func main กับ ส่วน Start Server ให้อยู่ตัว Goroutine ให้มันเป็นคนละ Process
func main() { fmt.Println("สวัสดี") mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`death`)) }) http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`pingkunga`)) }) srv := http.Server{ Addr: ":10170", Handler: mux, } //แยกอยู่ Process go func() { log.Fatal(srv.ListenAndServe()) }() fmt.Println("server starting at :10170") shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) <-shutdown fmt.Println("shutting down...") if err := srv.Shutdown(context.Background()); err != nil { //ตรงนี้จะทำอะไรก็ทำ ทำ Request ที่รับมาแล้ว //บอก K8S ฝากลูกเมียข้าด้วย ให้เรียบร้อยยย //เฮือกกกกก fmt.Println("shutdown err:", err) } fmt.Println("bye bye") }
dotnet ต้องไปใช้ตัว OnStopping()">IHostedService > OnStopping() นะ ต้องไปทำตาม Framework ของ Go ดู Simple กว่าใช้ Goroutine + Channel + os.Signal
แปะ Implementing Graceful Shutdown in Go | RudderStack | RudderStack
8. Go Packages
มันการจัดการกลุ่มของงานตาม Domain (DDD) แหละ โดยใน Go แยกเป็น Package ซึ่งอีกตาม Domain แล้วข้างในมีไฟล์ที่ทำหน้าที่เฉพาะของมัน
เดิมทุก logic ของ user อยู่ที่นี้หมดเลย - server.go
ใหม่แยกตาม domain - user --> userDB --ต่อ DB --> userHandler --จัดการ request ต่างๆตาม logic --> user --พวก dto / class - server.go --เหลือส่วนขึ้น server echo
เหมือนได้ยินว่า package ซ้อนกันไม่ใช่แม่ลูกกันนะ
Code: learn-go/API/6Package/API at main · pingkunga/learn-go (github.com)
9. How to build Go
- Local
- go build
Note: go run //เบื่องหลังมันไปเรียก go build
go build -o sample.exe . //. ไฟล์ที่อยู่ใน path นั้นๆ go build -o sample.exe server.go
- Specific Platform Build
//Specific Platform // - List Support platform go tool dist list //Set Arch + platform then build GOOS=linux GOARCH=amd64 go build
- Docker Build
- ตัว docker file แบบ multi stage
Image golang - Official Image | Docker Hub
FROM golang:1.22.1-alpine3.19 AS build WORKDIR /app COPY . ./ RUN go mod download RUN go build -o /bin/app FROM alpine:3.19.1 COPY --from=build /bin/app /bin EXPOSE 10170 CMD [ "/bin/app" ]
- Build & Run หรือ ใช้ Buildx แก้ Arch+Platform
// BUILD docker build -t go-app . //RUN docker run go-ap
Reference
- How to correctly use Basic Authentication in Go – Alex Edwards
- go - How to ignore one router for BasicAuth check with Echo framework? - Stack Overflow
- lib/pq: Pure Go Postgres driver for database/sql (github.com)
- GitHub ของ Blog นี้ learn-go/API at main · pingkunga/learn-go (github.com)
Discover more from naiwaen@DebuggingSoft
Subscribe to get the latest posts sent to your email.