Mọi server HTTP thực tế cần các lớp xử lý cắt ngang: ghi log, xác thực, CORS, nén gzip, giới hạn tần suất, phục hồi panic. Nhồi tất cả vào từng handler là lặp và rối. Go giải bài này bằng một mẫu thanh lịch dựa trên hàm bậc cao: middleware — một hàm nhận http.Handler và trả về http.Handler bọc thêm hành vi. Xâu chuỗi chúng lại cho ta một pipeline xử lý gọn, tái dùng được. Bài này mổ xẻ cơ chế củ hành, cách một middleware dừng chuỗi, một combinator để kết hợp, và đo thật chi phí thực sự.
Middleware là hàm bọc Handler
Định nghĩa chuẩn: Middleware là kiểu func(http.Handler) http.Handler. Nó nhận handler "tiếp theo" và trả về một handler mới thực hiện việc của nó rồi gọi handler tiếp theo:
type Middleware func(http.Handler) http.Handler
func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("vào")
next.ServeHTTP(w, r) // gọi lớp trong -> mô hình củ hành
fmt.Println("ra") // chạy SAU khi lớp trong xong
})
}
Đây là mô hình củ hành (onion): code trước next.ServeHTTP chạy lúc request đi vào, code sau chạy lúc response đi ra. Nhờ closure bắt giữ next, mỗi lớp bọc lớp trong nó, tạo thành các vòng đồng tâm quanh handler lõi.

Hình 1: Middleware là func(http.Handler) http.Handler. Log in "vào" trước next.ServeHTTP và "ra" sau — mô hình củ hành. Auth short-circuit bằng cách return không gọi next. Chain kết hợp nhiều middleware giữ đúng thứ tự khai báo.
Short-circuit: dừng chuỗi giữa chừng
Sức mạnh của mẫu này là một middleware có thể không gọi next — dừng toàn bộ chuỗi và trả response luôn. Đây là cách Auth chặn request thiếu token:
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Token") == "" {
http.Error(w, "cần token", 401)
return // KHÔNG gọi next -> handler chính KHÔNG chạy
}
next.ServeHTTP(w, r)
})
}
Chain: kết hợp giữ đúng thứ tự
Xâu chuỗi thủ công Log(Auth(handler)) đọc ngược và khó khi có nhiều lớp. Một combinator Chain gói việc đó, giữ thứ tự khai báo tự nhiên:
func Chain(mws ...Middleware) Middleware {
return func(final http.Handler) http.Handler {
for i := len(mws) - 1; i >= 0; i-- { // bọc từ TRONG ra
final = mws[i](final)
}
return final
}
}
app := Chain(Log, Auth)(handler) // Log -> Auth -> Handler
Vòng lặp bọc từ cuối lên đầu, để middleware đầu tiên trong danh sách chạy đầu tiên (ngoài cùng). Đo thật (Go 1.23):

Hình 2: Request có token — thứ tự củ hành: Log vào → Auth OK → Handler → Log ra. Request thiếu token — Auth từ chối (401), Handler chính không chạy, nhưng Log ra vẫn chạy (nó nằm sau ServeHTTP). Benchmark: 5 middleware (80,22 ns) gần như bằng handler trần (81,85 ns).
Kết quả chạy: với token, thứ tự là Log vào → Auth OK → Handler → Log ra — đúng mô hình củ hành. Thiếu token, Auth từ chối và trả 401, Handler chính không chạy, nhưng Log ra vẫn chạy vì nó nằm sau lời gọi next.ServeHTTP (mà Log vẫn gọi Auth; chính Auth mới không gọi handler lõi).
Đo thật: chuỗi middleware gần như miễn phí
Câu hỏi công bằng: mỗi lớp middleware là một lời gọi hàm — chồng 5-10 lớp có chậm không? Benchmark handler trần so với chuỗi 5 middleware:
- không middleware: 81,85 ns/op, 208 B, 4 cấp phát.
- chuỗi 5 middleware: 80,22 ns/op, 208 B, 4 cấp phát.
Gần như không khác biệt (chênh trong nhiễu, cùng cấp phát). Lý do: chuỗi được dựng một lần lúc khởi tạo server (app := Chain(...)(handler)), tạo ra các closure lồng nhau cố định. Mỗi request chỉ gọi qua chuỗi đã dựng sẵn — mỗi lớp là một lời gọi hàm mà Go tối ưu rất tốt. (208 B/4 cấp phát ở đây chủ yếu là httptest.NewRecorder trong benchmark, không phải middleware.) Kết luận: đừng lo về chi phí xâu chuỗi middleware; nó không đáng kể.
Đánh đổi cần cân nhắc
Chuẩn func(http.Handler) http.Handler khớp toàn hệ sinh thái. Vì middleware của bạn dùng đúng chữ ký http.Handler chuẩn, nó cắm được vào bất kỳ router nào (chi, gorilla/mux, net/http thuần) và tái dùng across nhiều server. Đừng phát minh chữ ký middleware riêng (nhận *Context tùy biến) trừ khi thật sự cần — nó khóa bạn vào framework và mất tính tương thích. Router như chi xây trọn vẹn quanh chuẩn này.
Thứ tự middleware quan trọng và dễ sai. Recover (phục hồi panic) phải ngoài cùng để bắt panic từ mọi lớp trong; Log thường ngoài để đo cả thời gian; Auth trước handler nghiệp vụ; Gzip gần lõi. Đặt sai thứ tự gây bug tinh vi (panic không được bắt, log thiếu thời gian auth). Chain giữ thứ tự khai báo rõ ràng, nhưng bạn vẫn phải suy nghĩ về thứ tự đó.
Truyền dữ liệu giữa middleware qua context, không qua biến chung. Khi một middleware (auth) cần chuyển dữ liệu (user id) cho handler, dùng r.Context() với context.WithValue — không dùng biến toàn cục hay field trên struct dùng chung (không an toàn với nhiều request đồng thời). Đây là lý do context.Context tồn tại; middleware là nơi dùng nó đúng nhất.
Ba ý mang về
- Middleware trong Go là
func(http.Handler) http.Handler— hàm bậc cao bọc thêm hành vi quanh handler theo mô hình củ hành: code trướcnext.ServeHTTPchạy lúc vào, code sau chạy lúc ra (đo thậtLog vào → Auth → Handler → Log ra). - Middleware short-circuit được: không gọi
nextlà dừng chuỗi và bỏ qua handler lõi (đo thậtAuththiếu token trả 401, handler chính không chạy) — nền tảng cho auth, rate limit, cache;Chaincombinator kết hợp nhiều lớp giữ đúng thứ tự khai báo. - Chi phí gần như bằng không: đo thật 5 lớp middleware (80,22 ns) ngang handler trần (81,85 ns) vì chuỗi dựng một lần lúc khởi tạo — dùng chuẩn
http.Handlerđể khớp cả hệ sinh thái, và truyền dữ liệu giữa lớp quacontext.
Phần sau ta xét cách mở rộng chương trình bằng các thành phần nạp động: Phần sau mổ xẻ plugin architecture trong Go — từ package plugin chính thức tới cách dựng hệ thống mở rộng bằng interface đăng ký, và vì sao Go ít dùng plugin nạp lúc chạy.