Middleware trong Go không cần framework. Nó là hệ quả trực tiếp của việc http.Handler là interface một method.
Bảy dòng
func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t0 := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(t0))
})
}
Đó là toàn bộ khái niệm: hàm nhận Handler, trả về Handler. Không có chú thích, không có đăng ký, không có thứ tự cấu hình ẩn.
Dùng:
http.ListenAndServe(":8080", Log(mux))
Xâu chuỗi
h := Log(PhucHoi(XacThuc(mux)))
Đọc từ ngoài vào trong: Log chạy trước, rồi PhucHoi, rồi XacThuc, rồi mux. Trên đường về thì ngược lại.
Chuỗi dài thì viết hàm gộp:
func Chain(h http.Handler, ms ...func(http.Handler) http.Handler) http.Handler {
for i := len(ms) - 1; i >= 0; i-- {
h = ms[i](h)
}
return h
}
h := Chain(mux, Log, PhucHoi, XacThuc)
Lặp ngược để thứ tự đọc từ trái sang phải khớp thứ tự chạy.
Middleware bắt buộc phải có
Phục hồi panic — bài 25 đã nói. net/http có recover sẵn nhưng chỉ in ra stderr:
func PhucHoi(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic: %v\n%s", rec, debug.Stack())
http.Error(w, "lỗi máy chủ", 500)
}
}()
next.ServeHTTP(w, r)
})
}
debug.Stack() là phần quan trọng — không có nó bạn chỉ biết "có panic".
Mã tương quan để lần theo request qua log:
func MaRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ma := r.Header.Get("X-Request-ID")
if ma == "" { ma = uuid.New().String() }
ctx := context.WithValue(r.Context(), khoaMa{}, ma)
w.Header().Set("X-Request-ID", ma)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Chú ý r.WithContext(ctx) — Request bất biến với context, nên phải tạo bản mới. Đây là chỗ WithValue thật sự chính đáng, đúng như bài 29 đã nói.
Giới hạn kích thước thân — http.MaxBytesReader, bài 45.
Bắt mã trạng thái để log
http.ResponseWriter không cho đọc lại mã trạng thái đã ghi. Muốn log nó thì phải bọc:
type ghiLai struct {
http.ResponseWriter
ma int
}
func (g *ghiLai) WriteHeader(ma int) {
g.ma = ma
g.ResponseWriter.WriteHeader(ma)
}
Đây là embedding interface ở bài 15: ghiLai nhúng http.ResponseWriter nên tự động thoả mãn nó, và bạn chỉ ghi đè method cần đổi.
gl := &ghiLai{ResponseWriter: w, ma: 200} // mặc định 200 vì handler có thể không gọi WriteHeader
next.ServeHTTP(gl, r)
log.Printf("%d %s %s", gl.ma, r.Method, r.URL.Path)
Một cảnh báo: bọc ResponseWriter làm mất các interface tuỳ chọn như http.Flusher và http.Hijacker. Với endpoint stream hay WebSocket, bạn phải chuyển tiếp chúng tường minh — đây là hạn chế thật của mẫu này.
Khi nào cần router bên ngoài
Từ Go 1.22, ServeMux đủ cho phần lớn API — bài 46 đã cho thấy nó hiểu method và tham số đường dẫn.
Cân nhắc chi hoặc gin khi cần:
Nhóm route với middleware riêng — /api/v1/* dùng xác thực, /health thì không. Với ServeMux bạn phải tự dựng nhiều mux.
Ràng buộc tham số — regex trên path variable.
Middleware theo route thay vì theo toàn bộ server.
chi đáng chú ý vì nó tương thích hoàn toàn với http.Handler — middleware bạn viết dùng được ở cả hai nơi, và bỏ nó ra sau này không phải viết lại gì.
gin nhanh hơn nhưng dùng kiểu gin.Context riêng, nên mã handler của bạn bị khoá vào nó. Đó là đánh đổi cần biết trước.
Lời khuyên của tôi cho dự án mới: bắt đầu với ServeMux. Nó không có phụ thuộc, và nếu sau này cần router mạnh hơn thì handler viết theo http.Handler chuyển sang được gần như không sửa.
Thử ba mươi giây
Thêm middleware log vào dịch vụ của bạn và in cả thời gian lẫn mã trạng thái:
log.Printf("%d %s %s %v", gl.ma, r.Method, r.URL.Path, time.Since(t0))
Chạy vài phút dưới tải thật rồi sắp xếp theo thời gian. Endpoint chậm nhất thường không phải cái bạn đoán — và đó là thông tin bạn có được sau ba mươi giây viết mã.
Ngày mai: database/sql — pool kết nối và những chỗ nó âm thầm khác Java.