โหมดมืด
บทที่ 13 — Web Framework Deep (Gin + Echo + Fiber + Chi)
← บทที่ 12b: Production Playbook | สารบัญ | บทที่ 14: Database in Go →
🚀 โซนขั้นสูง — มือใหม่ข้ามได้
บทนี้เป็น เนื้อหาขั้นสูง สำหรับคนที่จบบทพื้นฐาน (0–8) และเข้าใจ HTTP server ใน บท 11 มาแล้ว — มันลงรายละเอียด framework (เฟรมเวิร์ก = ชุดเครื่องมือสำเร็จรูปสำหรับสร้างเว็บ) หลายตัวพร้อมศัพท์เฉพาะเยอะ ถ้าเพิ่งเริ่ม Go ข้ามบทนี้ไปก่อนได้ แล้วกลับมาอ่านตอนจะทำเว็บจริง
จาก บท 11 — Standard Library คุณรู้แล้วว่า net/http ของ Go สร้าง web server (เซิร์ฟเวอร์เว็บ = โปรแกรมที่คอยรับ request แล้วตอบกลับ) ได้
แต่ใน production (ระบบที่ใช้งานจริง) — เราใช้ framework เพื่อ:
- Routing (การจับคู่ URL กับโค้ดที่จะทำงาน) ที่เร็ว + ยืดหยุ่น
- Middleware system (ระบบฟังก์ชันคั่นกลางก่อน/หลัง handler เช่น auth (การตรวจสอบตัวตน), log (บันทึกการทำงาน))
- Validation (ตรวจสอบความถูกต้องของข้อมูลที่ส่งเข้ามา)
- Error handling (การจัดการข้อผิดพลาด)
- JSON binding (แปลง JSON ใน request เข้า struct อัตโนมัติ)
- Performance optimization (ปรับให้ทำงานเร็ว)
บทนี้สอน framework ที่นิยม:
- Gin — นิยมที่สุด (most popular)
- Echo — API สะอาด (clean)
- Fiber — เร็วที่สุด (สร้างบน fasthttp)
- Chi — เล็กและเป็น idiomatic (สำนวน Go แท้)
- net/http + chi = สไตล์ stdlib (ใช้ของในตัวภาษา)
🔄 ต่างจาก C#: ASP.NET Core คือ web framework ทางการหนึ่งเดียวของ .NET (มี Kestrel + ASP.NET Core ให้ใช้เสมอ) แต่ Go ไม่มี framework ทางการ — ทีม Go ตั้งใจให้
net/httpมีแค่พื้นฐานเรียบง่าย แล้วปล่อยให้ชุมชนแข่งกันสร้าง Gin/Echo/Fiber/Chi ไม่มีตัวไหน "blessed" โดยทีม Go เอง อย่าคาดว่า Gin จะเทียบเท่า ASP.NET Core Minimal API แบบ 1:1 — คาดหวังว่าจะเจอการเปลี่ยน framework ที่นิยมบ่อยกว่าที่คุ้นเคยใน .NET
⚠️ Framework ที่ควรเลี่ยง:
gorilla/muxเคยเป็นตัวเลือกยอดนิยม แต่ถูก archive (เก็บไม่ maintain แล้ว) ตั้งแต่ Aug 2022 — โปรเจกต์ใหม่ควรใช้ chi หรือ net/http ServeMux ของ Go 1.22+ แทน ทั้งคู่รองรับ method matching + path parameter สำหรับกรณีทั่วไป ส่วน chi มี features เพิ่มเติมที่ net/http ServeMux ยังไม่มี เช่น:
- inline regexp — กำหนด pattern ของ path parameter ด้วย regular expression ได้ตรง ๆ ในเส้น route
- middleware stack ต่อ group — ผูก middleware เฉพาะกลุ่ม route หนึ่ง ๆ ได้ ไม่ต้องใช้ทั้งแอป
- sub-router — แยก router ย่อยไปประกอบเป็น route ใหญ่ได้ (เหมาะกับแอปที่มีหลายโมดูล)
ใช้เวลา 3-4 ชั่วโมง
1. Why Framework
1.1 ข้อจำกัดของ net/http
ก่อนจะรู้ว่าทำไมต้องใช้ web framework ลองดูว่า net/http ล้วน ๆ ขาดอะไร — เราต้องแยก path เอง, เช็ค method เอง, ไม่มี middleware chain, ไม่มี validation, ไม่มี panic recovery (การดักจับ panic ไม่ให้แอปพังทั้งหมด — ต่างจาก error ตรงที่ panic คือความผิดพลาดร้ายแรงที่โปรแกรมหยุดทำงานทันทีถ้าไม่ดักไว้ ดูรายละเอียด panic vs error ใน บท 7 — Errors) สิ่งเหล่านี้คือเหตุผลที่ framework เกิดขึ้น:
go
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" { ... }
parts := strings.Split(r.URL.Path, "/")
id := parts[2] // manual parsing!
// ไม่มี middleware chain
// ไม่มี validation
// ไม่มี panic recovery
})→ ต้องเขียนทุกอย่างเอง
1.2 ทำไม Go 1.22+ ดีขึ้น
ข่าวดีคือ Go 1.22 ปรับปรุง ServeMux ในตัวให้รองรับการระบุ method และ path parameter ได้แล้ว (GET /users/{id}) ลดความจำเป็นของ framework ลงสำหรับงานง่าย ๆ — แต่ middleware, validation, JSON binding ยังต้องทำเอง ซึ่งเป็นจุดที่ framework ยังช่วยได้:
go
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", handleGetUser)
mux.HandleFunc("POST /users", handleCreateUser)
func handleGetUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
}→ Go 1.22 stdlib เพิ่ม method + path parameter ใน ServeMux
แต่ middleware, validation, JSON binding ยังต้องเอง
2. Gin
2.1 Hello
Gin เป็น web framework ที่นิยมที่สุดของ Go — เร็ว มี ecosystem ใหญ่ (ecosystem = ชุมชนและ library เสริมที่ทำงานร่วมกับ Gin ได้ เช่น middleware สำเร็จรูปสำหรับ JWT, CORS, rate limit) และ API กระชับ
วิธีเริ่มคือเรียก gin.Default() = สร้าง Gin พร้อม logger (บันทึก request อัตโนมัติ) + recovery (ดักจับ panic ไม่ให้แอปพังทั้งหมด) แล้วผูก route เข้ากับ handler ที่รับ *gin.Context (ออบเจ็กต์ที่ห่อ request/response):
bash
go get github.com/gin-gonic/gingo
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id, "name": "Alice"})
})
r.Run(":8080")
}2.2 Request Binding
Gin แปลง JSON ใน request body เข้า struct ให้อัตโนมัติด้วย ShouldBindJSON (ลดงานเขียน parse + validate เอง)
ตรวจ validation ด้วย tag binding:"..." บน field ของ struct — ตัวที่ใช้บ่อย: required (ห้ามว่าง), email (ต้องเป็นอีเมล), min/max (ความยาวต่ำสุด/สูงสุด), gte/lte (ค่ามากกว่า-เท่ากับ/น้อยกว่า-เท่ากับ):
🔄 ต่างจาก C#: struct tag แบบ
binding:"required,email"ทำหน้าที่เดียวกับ DataAnnotations ของ C# เช่น[Required],[EmailAddress],[Range(0,150)]บน DTO (หรือใช้ FluentValidation ก็ได้) — แนวคิดเหมือนกันคือ "แปะ metadata บน field แล้วให้ framework ตรวจให้"
go
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2,max=100"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=150"`
}
r.POST("/users", func(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
user := userService.Create(req)
c.JSON(201, user)
})2.3 Middleware
Middleware คือฟังก์ชันที่ทำงาน "คั่นกลาง" ก่อน/หลัง handler — ใช้ทำ auth, logging, recovery (กู้คืนจาก panic) ฯลฯ
ใน Gin เขียน middleware เป็น gin.HandlerFunc แล้วผูกด้วย Use() (ใช้ทั้งแอป) หรือผูกกับ Group เฉพาะกลุ่ม route (เช่น /admin/*):
🔄 ต่างจาก C#: แนวคิดนี้เหมือน ASP.NET Core middleware pipeline เกือบทุกจุด —
app.Use(async (ctx, next) => { ...; await next(); })ของ C# กับc.Next()ของ Gin ทำหน้าที่เดียวกันคือ "เรียก middleware/handler ตัวถัดไปในสาย"
go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if !valid(token) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Set("userId", extractUserId(token))
c.Next()
}
}
r := gin.New()
r.Use(gin.Logger(), gin.Recovery(), AuthMiddleware())
// per-route
admin := r.Group("/admin", AdminOnlyMiddleware())
admin.GET("/users", listUsers)2.4 Error Handling
แทนที่จะจัดการ error กระจัดกระจายในทุก handler Gin ให้ handler "สะสม" error ไว้ (c.Error()) แล้วใช้ middleware กลางแปลง error เป็น HTTP response ที่จุดเดียว — สม่ำเสมอและลดโค้ดซ้ำ:
🔄 ต่างจาก C#: เทียบได้กับ
UseExceptionHandlermiddleware หรือIExceptionHandler(.NET 8+) ของ ASP.NET Core ที่มักคู่กับProblemDetailsเพื่อให้ error response หน้าตาสม่ำเสมอทั้งแอป
go
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// APIError ต้อง implement interface error เพื่อให้ errors.As ทำงานได้
func (e *APIError) Error() string { return e.Message }
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
var apiErr *APIError
if errors.As(err, &apiErr) {
c.JSON(apiErr.Code, apiErr)
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"})
}
}
}
r.GET("/users/:id", func(c *gin.Context) {
user, err := svc.Get(c.Param("id"))
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, user)
})2.5 Performance
เหตุผลหนึ่งที่ Gin นิยมคือความเร็ว — เบื้องหลังใช้ httprouter (library จัดการ routing ที่ Gin ใช้เบื้องหลัง) ซึ่งจับคู่ route ด้วยโครงสร้างข้อมูลแบบ radix tree (= โครงสร้างข้อมูลที่จัดกลุ่ม URL ที่ขึ้นต้นเหมือนกันไว้ด้วยกัน ทำให้ค้นหา route ตรงกับ URL ที่เข้ามาได้เร็วโดยไม่ต้องเทียบทีละเส้น) การจับคู่แบบนี้หา route เจอเร็วมาก รองรับได้หลายหมื่น request ต่อวินาทีต่อ core
Benchmark: 50K+ req/s ต่อ core (ตัวเลขขึ้นกับ hardware, payload size, และ handler complexity — ใช้เป็นแนวทางเท่านั้น ไม่ใช่การรับประกัน)
3. Echo
3.1 Hello
Echo เป็น framework ที่คล้าย Gin แต่มีจุดเด่นคือ handler "คืน error" ได้ (func(c) error) ทำให้จัดการ error สะอาดกว่า และมี middleware ทางการให้ครบ (JWT, CORS, recovery):
go
package main
import "github.com/labstack/echo/v4"
func main() {
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(200, map[string]any{"id": id})
})
e.Logger.Fatal(e.Start(":8080"))
}3.2 ความต่างจาก Gin
แม้ใช้งานคล้ายกัน Echo มีจุดที่ต่างจาก Gin ชัดเจน — สรุปให้เห็นว่าควรเลือกตัวไหนตามความต้องการ:
- Return error จาก handler — cleaner error path
- Built-in HTTP/2 + WebSocket
- Better request context
- Recovery, CORS, JWT — official middleware
3.3 Group + Middleware
Echo จัดกลุ่ม route ที่ใช้ middleware ร่วมกันได้ (e.Group) — เช่นกลุ่ม /api ที่ต้องผ่าน JWT ทุก route ช่วยจัดระเบียบและไม่ต้องใส่ middleware ซ้ำทุกเส้น:
go
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
api := e.Group("/api", middleware.JWTWithConfig(jwtConfig))
api.GET("/users", listUsers)
api.POST("/users", createUser, requireAdmin)3.4 Binding + Validation
Echo แยกขั้นตอน bind (แปลง request เข้า struct) กับ validate ออกจากกัน — เรา Bind ก่อนแล้วค่อย Validate โดยติดตั้ง validator (เช่น go-playground/validator) เองผ่าน e.Validator
e.Validator ต้องการ struct ที่มี method Validate(any) error (ใช้หลักการ interface จาก บท 6 — Interfaces — struct ไหนก็ตามที่มี method ครบตามที่ interface ต้องการ ถือว่า "implement" interface นั้นได้เลยโดยไม่ต้องประกาศชัดเจนแบบ C#) — CustomValidator ด้านล่างคือตัวอย่างของ struct แบบนั้น:
go
type CreateUser struct {
Name string `json:"name" validate:"required,min=2"`
Email string `json:"email" validate:"required,email"`
}
// CustomValidator = ตัวเชื่อม (adapter) ที่ห่อ go-playground/validator ให้ Echo เรียกใช้ผ่าน interface e.Validator ได้
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i any) error {
return cv.validator.Struct(i)
}
e.Validator = &CustomValidator{validator: validator.New()}
e.POST("/users", func(c echo.Context) error {
var req CreateUser
if err := c.Bind(&req); err != nil { return err }
if err := c.Validate(&req); err != nil { return err }
// ...
})4. Fiber
4.1 Hello
Fiber เป็น framework ที่เร็วที่สุดในกลุ่ม Go web framework — สร้างบน fasthttp (HTTP library ทางเลือก เน้นความเร็ว ไม่ใช่ net/http ของ stdlib)
API ออกแบบให้คล้าย Express.js ของ Node.js — ถ้าไม่เคยใช้ Node.js ก็ไม่เป็นไร คิดว่าเป็นแบบ Gin แต่เน้นความเร็วสูงสุด (แต่ดูข้อควรระวังเรื่องความเข้ากันได้กับ library อื่นในหัวข้อ 4.3):
bash
go get github.com/gofiber/fiber/v3go
package main
import "github.com/gofiber/fiber/v3"
func main() {
app := fiber.New()
app.Get("/users/:id", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"id": c.Params("id")})
})
app.Listen(":8080")
}⚠️ หมายเหตุเวอร์ชัน v3: handler ของ v3 ใช้
fiber.Ctx(interface ไม่ใช่ pointer) ต่างจาก v2 ที่ใช้*fiber.Ctx— ถ้าติดตั้งผิดเวอร์ชัน type จะไม่ match ควรระบุ minor version pin ที่go.mod(ไม่ใช่latest) และเช็คที่ gofiber.io สำหรับสถานะล่าสุดของ v3 ก่อนใช้จริง — ถ้าไม่แน่ใจว่า v3 GA แล้วหรือยัง โค้ดและ tutorial ส่วนใหญ่ที่หาเจอทั่วไปยังเป็น v2 (*fiber.Ctx) อยู่
GA (Generally Available) = เวอร์ชันเสถียรพร้อมใช้จริง (ไม่ใช่ beta/RC — RC ย่อจาก Release Candidate แปลว่ารุ่นทดสอบก่อนออกจริง อาจยังมีการเปลี่ยนแปลงก่อนปล่อยตัวจริง) ตรวจได้โดยดูที่ GitHub Releases ว่ามี tag vX.X.X ที่ไม่มีคำว่า beta/rc หรือไม่
4.2 จุดเด่น
- API ใกล้ Express.js (ดีสำหรับ Node dev)
- Built on fasthttp — เร็วกว่า net/http 2-3x
- เรียนรู้ง่าย
4.3 ⚠️ Caveat
fasthttp ≠ net/http — fasthttp คือ HTTP library คนละตัวกับ net/http ของ stdlib (เน้นความเร็ว แต่ไม่ใช้ type มาตรฐานของ Go) หมายความว่า library อื่นที่เขียนมาสำหรับ net/http (เช่น OpenTelemetry, บาง middleware) จะใช้กับ Fiber ตรง ๆ ไม่ได้ ต้องมี adapter หรือรอ library รองรับ fasthttp โดยเฉพาะ ผลตามมา:
- ไม่ใช้ standard
net/http.Request - Middleware ของ stdlib ใช้ไม่ได้ตรง ๆ
- Library บางตัว (OpenTelemetry, etc) อาจไม่ support
→ ใช้เมื่อต้องการความเร็วเหนือกว่าความเข้ากันได้กับ library อื่น ๆ ใน ecosystem
5. Chi (minimal idiomatic)
5.1 Hello
Chi เป็น framework ที่ "เป็น Go มากที่สุด" — handler และ middleware ใช้ type มาตรฐานของ net/http ตรง ๆ (ไม่มี Context type ของตัวเอง)
ข้อดี: ใช้ library ของ stdlib ได้ทุกตัว เหมาะกับคนที่ชอบ idiomatic Go และไม่อยากผูกกับ API เฉพาะของ framework ตัวใดตัวหนึ่ง:
go
package main
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
json.NewEncoder(w).Encode(map[string]string{"id": id})
})
http.ListenAndServe(":8080", r)
}5.2 จุดเด่น
- stdlib-compatible — handler =
http.HandlerFunc - Middleware =
func(http.Handler) http.Handler - ใช้ library ของ net/http ได้ทุกตัว
- Trie router (fast)
→ recommended สำหรับคนที่ชอบ idiomatic Go
6. เปรียบเทียบ
| Gin | Echo | Fiber | Chi | net/http (1.22+) | |
|---|---|---|---|---|---|
| Popularity | Highest | High | High | Mid | Built-in |
| Performance | Fast | Fast | Fastest | Fast | Good |
| API style | Custom Context | Custom + error return | Express-like | stdlib | stdlib |
| Compatibility | Custom | Custom | Custom (fasthttp) | stdlib | stdlib |
| Middleware | Rich | Rich | Rich | Composable stdlib | Compose manual |
| Validation | go-playground | go-playground | go-playground | self / external | self |
| WebSocket | extension | built-in | built-in | extension | external |
Recommendation:
- New project, simple: Chi (idiomatic) หรือ net/http (Go 1.22+)
- New project, full-featured: Echo
- Existing Gin code: Gin (don't migrate)
- Performance critical: Fiber (ระวัง compatibility)
7. Production Patterns
7.1 Graceful Shutdown
ไม่ว่าใช้ framework ไหน production server ต้องปิดอย่างนุ่มนวล — รอ request ที่ค้างเสร็จก่อนปิดเมื่อได้รับสัญญาณหยุด (SIGTERM) ตัวอย่างนี้ใช้กับ Gin แต่หลักการเดียวกันกับทุก framework:
go
func main() {
r := gin.Default()
// ... routes
srv := &http.Server{Addr: ":8080", Handler: r}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("shutdown:", err)
}
}7.2 Request ID + Logging
ทำไมต้องมี request ID (รหัสเฉพาะของ request หนึ่งครั้ง)? เพื่อให้ตามรอย log ของ request เดียวกันข้ามหลาย ๆ บริการได้ — สำคัญมากเวลา debug ปัญหาใน production
วิธีทำ: สร้าง request ID ที่ middleware ต้นทาง (อ่านจาก header X-Request-Id ถ้ามี หรือ generate ใหม่ด้วย uuid) แล้วแนบกับทุก log ของ request นั้นผ่าน slog.Logger ที่ติด field req_id:
bash
go get github.com/google/uuidgo
import (
"log/slog"
"github.com/google/uuid"
)
r.Use(func(c *gin.Context) {
reqId := c.GetHeader("X-Request-Id")
if reqId == "" { reqId = uuid.NewString() }
c.Set("requestId", reqId)
c.Header("X-Request-Id", reqId)
// ผูก req_id ติดกับ logger เพื่อให้ทุก log ของ request นี้มี field req_id
logger := slog.Default().With("req_id", reqId)
c.Set("logger", logger)
c.Next()
})7.3 Timeout
ตั้ง timeout ให้ทุก request เพื่อไม่ให้ handler ที่ค้าง (เช่น query ช้า) ทำให้ทรัพยากรหมด — เกินเวลาที่กำหนดก็ตัดและตอบ error
⚠️ ก่อนใช้ — กับดักของ Gin timeout middleware:
gin-contrib/timeoutมีประวัติ data race (การที่ goroutine หลายตัวอ่าน/เขียนข้อมูลเดียวกันพร้อมกันจนผลเพี้ยน) / panic ใน edge case (เช่นเขียน response หลัง timeout) ตรวจ issue ของ repo ก่อนใช้ productionทางที่ปลอดภัยกว่า: ใช้
context.WithTimeoutในตัว handler เอง แล้วส่ง context ต่อไปยัง downstream call ทั้งหมด (DB, HTTP client, gRPC) — handler หยุดทำงานเมื่อ context done
bash
go get github.com/gin-contrib/timeoutgo
import "github.com/gin-contrib/timeout"
r.Use(timeout.New(
timeout.WithTimeout(5*time.Second),
timeout.WithHandler(handler),
))วิธีที่ปลอดภัยกว่า (ใช้ context ในตัว handler):
go
r.GET("/slow", func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
// svc.DoSomething ต้องรับ ctx และตรวจ ctx.Done() ภายใน
// (เช่น ส่ง ctx ต่อไปยัง DB query, HTTP call) —
// context timeout ไม่หยุด goroutine ที่ไม่ตรวจ ctx.Done()
result, err := svc.DoSomething(ctx)
// ...
})7.4 Rate Limit
จำกัดจำนวน request ต่อช่วงเวลา (rate limit) ป้องกันการถูกยิงถล่มหรือใช้ทรัพยากรเกิน — เช่นอนุญาต 100 request/วินาที ส่วนเกินจะถูกปฏิเสธ:
bash
go get github.com/didip/tollbooth
go get github.com/didip/tollbooth_gingo
limiter := tollbooth.NewLimiter(100, nil) // 100 req/s
r.GET("/api/*action", tollbooth_gin.LimitHandler(limiter), handler)7.5 CORS
ถ้า frontend อยู่คนละ origin กับ API เบราว์เซอร์จะบล็อกด้วยกฎ CORS (Cross-Origin Resource Sharing = กฎความปลอดภัยของเบราว์เซอร์ที่บล็อกการเรียก API ข้ามโดเมน ต้องตั้ง header อนุญาตที่ฝั่ง server) — ต้องตั้ง middleware บอกว่า origin/method/header ไหนที่อนุญาต (อย่าใช้ * กับ API ที่มี auth):
go
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://app.acme.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))7.6 Structured Logging
ผูก log/slog เข้ากับ framework เพื่อให้ทุก request ถูก log แบบ structured (key-value/JSON) อัตโนมัติ — ค้นหาและวิเคราะห์ใน log aggregator ได้ง่าย:
bash
go get github.com/samber/slog-gingo
import (
"log/slog"
sloggin "github.com/samber/slog-gin"
)
r.Use(sloggin.New(slog.Default()))7.7 OpenTelemetry
เพิ่ม distributed tracing ให้ทุก request ด้วย middleware ของ OpenTelemetry — ติดตามเส้นทาง request ข้ามหลายบริการได้
ช่วย debug ปัญหา latency (เวลาที่ใช้ตอบ request) ในระบบ microservices (สถาปัตยกรรมที่แบ่งแอปเป็นบริการย่อย ๆ แต่ละตัวคุยกันผ่าน network):
go
import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
r.Use(otelgin.Middleware("my-service"))8. Project Structure
text
cmd/
api/
main.go # entry point
internal/
handler/
user.go # HTTP handlers
service/
user.go # business logic
repository/
user.go # data access
domain/
user.go # types
middleware/
auth.go
config/
config.go
pkg/ # reusable across project
httpserver/
go.mod
go.sum8.1 Handler → Service → Repository
go
// handler
func (h *UserHandler) Get(c *gin.Context) {
user, err := h.svc.GetByID(c, c.Param("id"))
if err != nil {
c.Error(err); return
}
c.JSON(200, user)
}
// service
func (s *UserService) GetByID(ctx context.Context, id string) (*User, error) {
return s.repo.FindByID(ctx, id)
}
// repository
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
// SQL / NoSQL
}9. Testing
go
import (
"encoding/json"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockService คือ struct ที่ implement interface ของ UserService สำหรับการทดสอบ
// (สร้างเองหรือใช้ tool เช่น mockery) ตัวอย่างนี้เป็น fragment แสดงแนวคิด
// ไม่ใช่ไฟล์ที่รันได้ทันที — ต้อง stub mockService ให้คืนค่าจริงก่อน เช่น
// mockService.On("GetByID", "1").Return(&User{ID: 1}, nil)
// User.ID ในตัวอย่างนี้เป็นชนิด int
func TestGetUser(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.Default()
h := NewUserHandler(mockService)
r.GET("/users/:id", h.Get)
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
var user User
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &user))
assert.Equal(t, 1, user.ID) // ID เป็น int ไม่ใช่ string
}10. Checkpoint
- Gin vs Echo vs Fiber vs Chi?
- ทำไม Fiber เร็วกว่า — caveat?
- Chi เหมาะกับโครงการแบบไหน?
- Graceful shutdown ทำงานยังไง?
- Middleware chain ใน Gin?
- Request ID propagation?
- CORS config สำคัญอะไร?
- Validation ใช้ tag อะไร?
- Project structure recommend?
- Go 1.22 stdlib ServeMux ใหม่ทำอะไรได้?
11. สรุปบทนี้
- Gin = popular default
- Echo = clean + error return
- Fiber = fastest (Express-like)
- Chi = stdlib idiomatic
- net/http 1.22+ = พอใช้ได้ + simple
- Production: graceful shutdown, request ID, timeout, CORS, OTel, slog
- Architecture: handler → service → repository
บทถัดไป — Database in Go (database/sql, sqlc, GORM)