Suốt sê-ri này ta đã mổ xẻ runtime, GC, scheduler, channel, chẩn đoán, hồ sơ hiệu năng. Bài capstone này gộp chúng lại thành thứ bạn thực sự triển khai: một REST API production-grade — không framework, chỉ thư viện chuẩn. Mục tiêu không phải "hello world" mà là một dịch vụ có đủ những gì cần cho sản xuất: định tuyến gọn, middleware chống sập, đếm an toàn dưới tải, timeout, và graceful shutdown. Rồi ta đo tải nó thật để thấy thư viện chuẩn Go mạnh đến đâu.
Định tuyến: net/http đủ dùng, không cần framework
Từ Go 1.22, http.ServeMux định tuyến được theo method + path — thứ trước đây phải cậy tới gorilla/mux hay chi. Không còn lý do kéo framework vào cho một API thường:
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks", listHandler)
mux.HandleFunc("POST /tasks", createHandler)
mux.HandleFunc("GET /health", healthHandler) // probe cho k8s
Store dùng map bảo vệ bằng sync.RWMutex (đọc dùng RLock cho phép nhiều reader song song, ghi dùng Lock). Handler đọc/ghi JSON bằng encoding/json, trả đúng mã trạng thái (201 Created cho POST, 400 cho JSON hỏng).
Ba trụ cột production: recover, atomic, graceful shutdown
Middleware recover là thứ tách một đồ chơi khỏi một dịch vụ thật. Mặc định, một panic trong handler sẽ giết cả tiến trình — một request lỗi hạ luôn nghìn request khác. Middleware bọc mọi handler trong recover():
func mid(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil { // một handler panic KHÔNG hạ cả server
log.Printf("panic phuc hoi: %v", err)
http.Error(w, "loi noi bo", http.StatusInternalServerError)
}
}()
reqCount.Add(1) // atomic, an toàn dưới nghìn goroutine
next.ServeHTTP(w, r)
})
}
reqCount là atomic.Int64 — mỗi request Go phục vụ trên một goroutine riêng, nên bộ đếm dùng chung phải atomic (bài đồng thời đã đo hậu quả của đếm không đồng bộ). Graceful shutdown dùng srv.Shutdown(ctx): khi nhận SIGTERM (Kubernetes gửi lúc rolling update), server ngừng nhận request mới nhưng chờ request đang xử lý xong trong hạn 5 giây rồi mới tắt — không rớt request giữa chừng.

Hình 1: Kiến trúc dịch vụ — định tuyến net/http theo method+path, middleware recover + đếm atomic, và graceful shutdown qua srv.Shutdown(ctx) khi nhận tín hiệu.
Đo tải thật: 16.000 req/s, không tinh chỉnh gì
Ta chạy server rồi bắn tải bằng một client Go 50 luồng đồng thời trong 5 giây (POST /tasks), đo thông lượng và độ trễ phân vị:
Tổng request : 80.570 (thành công 80.570, lỗi 0)
Thông lượng : 16.114 req/s
Độ trễ p50 : 1,041 ms
Độ trễ p95 : 12,03 ms
Độ trễ p99 : 30,967 ms
Độ trễ max : 111,836 ms
Con số nói lên nhiều điều. 16.114 req/s với 0 lỗi từ một server thư viện chuẩn không tinh chỉnh gì — Go cân tải cao nhờ mỗi request một goroutine rẻ, scheduler tự trải trên 10 core. Nhưng chú ý đuôi trễ: p50 chỉ ~1ms, nhưng p99 lên 31ms và max tới 112ms. Đó không phải nhiễu — đó là GC pause (bài GC đã đo) cộng tranh khóa RWMutex lúc cao điểm. Bài học kinh điển: đừng bao giờ đánh giá dịch vụ chỉ bằng trung bình hay p50; người dùng cảm nhận cái đuôi p99/max. Bộ đếm atomic khớp chính xác: /health báo req nhảy từ 1 lên 80.572 (1 lần trước + 80.570 tải + 1 lần gọi này) — không mất một lần đếm nào dù 50 goroutine cùng Add.

Hình 2: Đo thật — 80.570 request 0 lỗi ở 16.114 req/s (p50 1ms, p99 31ms, max 112ms do GC+khóa); bộ đếm atomic khớp 80.572; server sống sót qua 3 panic (mỗi lần trả 500); và tắt sạch khi nhận SIGTERM.
Sống sót qua panic và tắt sạch
Hai hành vi production quan trọng nhất, đo thật:
panic phuc hoi: assignment to entry in nil map
gọi /no lần 1 -> HTTP 500
gọi /no lần 2 -> HTTP 500
gọi /no lần 3 -> HTTP 500
gọi /ok sau 3 panic -> HTTP 200 (server VẪN SỐNG)
Ba lần gọi endpoint cố tình panic (ghi vào nil map) đều bị middleware bắt, trả 500, ghi log — và endpoint lành /ok sau đó vẫn trả 200. Server không chết dù handler panic liên tục. Và khi nhận tín hiệu tắt, log cho thấy nhận tín hiệu, tắt mềm trong 5s... rồi đã tắt sạch — Shutdown() chờ các request đang chạy hoàn tất thay vì cắt phập.
Đánh đổi cần cân nhắc
Thư viện chuẩn đủ cho phần lớn API — framework là để đổi lấy tiện lợi, không phải tốc độ. 16.000 req/s không tinh chỉnh chứng minh net/http không phải nút thắt. Framework (Gin, Echo, Fiber) cho bạn binding/validation/middleware sẵn và cú pháp gọn hơn, nhưng cũng thêm phụ thuộc và một lớp trừu tượng phải học. Với API vừa và nhỏ, thư viện chuẩn (từ Go 1.22 có routing method+path) thường là lựa chọn ít nợ kỹ thuật nhất. Chọn framework khi tiện lợi xứng đáng, không phải vì tin nó "nhanh hơn".
recover trong middleware là lưới an toàn, không phải cách xử lý lỗi. Nó ngăn một bug hạ cả tiến trình — nhưng một panic được recover vẫn là bug cần sửa, không phải luồng điều khiển bình thường. Đừng dùng panic/recover thay cho trả error. Và recover chỉ bắt panic trong cùng goroutine của handler; nếu handler spawn một goroutine con rồi nó panic, middleware không cứu được — goroutine con phải tự recover (bài rò rỉ/panic đã bàn).
Đuôi trễ p99 là thứ cần tối ưu, và nó đến từ GC + khóa. p50 1ms trông tuyệt, nhưng max 112ms nghĩa là có request chậm hẳn. Muốn kéo đuôi xuống: giảm cấp phát để GC chạy ít hơn (bài escape analysis, sync.Pool), thay RWMutex bằng sharding hoặc cấu trúc lock-free nếu khóa là điểm nóng (đo bằng pprof mutex/block trước khi sửa), và cân GOGC/GOMEMLIMIT. Đừng tối ưu mù — profile chỉ ra điểm nóng thật rồi mới sửa.
Ba ý mang về
- Thư viện chuẩn Go dựng được REST API production-grade không cần framework:
net/http(Go 1.22+) định tuyến method+path,RWMutexcho store, JSON chuẩn — đo thật 16.114 req/s với 0 lỗi trên 80.570 request, không tinh chỉnh gì. - Ba trụ cột biến đồ chơi thành dịch vụ thật: middleware recover (đo thật server sống sót qua 3 panic, mỗi lần trả 500), bộ đếm atomic (khớp chính xác 80.572 dưới 50 goroutine đồng thời), và graceful shutdown qua
srv.Shutdown(ctx)khi nhận SIGTERM (tắt sạch, không rớt request). - Nhìn đuôi trễ, không nhìn trung bình: đo thật p50 chỉ 1ms nhưng p99 31ms và max 112ms — cái đuôi đến từ GC pause và tranh khóa, và đó mới là thứ người dùng cảm nhận; tối ưu nó bằng giảm cấp phát, giảm tranh khóa, và cân GC — sau khi profile, không đoán.