Skip to content

บทที่ 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)
  • EchoAPI สะอาด (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/gin
go
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#: เทียบได้กับ UseExceptionHandler middleware หรือ 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/v3
go
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

fasthttpnet/httpfasthttp คือ 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. เปรียบเทียบ

GinEchoFiberChinet/http (1.22+)
PopularityHighestHighHighMidBuilt-in
PerformanceFastFastFastestFastGood
API styleCustom ContextCustom + error returnExpress-likestdlibstdlib
CompatibilityCustomCustomCustom (fasthttp)stdlibstdlib
MiddlewareRichRichRichComposable stdlibCompose manual
Validationgo-playgroundgo-playgroundgo-playgroundself / externalself
WebSocketextensionbuilt-inbuilt-inextensionexternal

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/uuid
go
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/timeout
go
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_gin
go
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-gin
go
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.sum

8.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

  1. Gin vs Echo vs Fiber vs Chi?
  2. ทำไม Fiber เร็วกว่า — caveat?
  3. Chi เหมาะกับโครงการแบบไหน?
  4. Graceful shutdown ทำงานยังไง?
  5. Middleware chain ใน Gin?
  6. Request ID propagation?
  7. CORS config สำคัญอะไร?
  8. Validation ใช้ tag อะไร?
  9. Project structure recommend?
  10. 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)


← บทที่ 12b | สารบัญ | บทที่ 14: Database in Go →