中间件本质上是在包装 Handler
Go 的 HTTP handler 是:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
中间件就是接收一个 handler,返回一个新的 handler。新 handler 可以在调用原 handler 前后做事情,比如记录日志、恢复 panic、检查登录状态、加请求 ID、统计耗时、设置响应头。
理解这个模式后,你会发现很多 Web 框架的中间件都不神秘。标准库也能写出清楚的中间件链。
中间件类型
type Middleware func(http.Handler) http.Handler
串联:
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
使用:
handler := Chain(mux,
Recover(logger),
RequestID(),
RequestLogger(logger),
)
顺序很重要。上面的顺序表示请求先经过 Recover,再经过 RequestID,再经过 RequestLogger,最后到 mux。响应返回时顺序反过来。
请求 ID 中间件
type contextKey string
const requestIDKey contextKey = "request_id"
func RequestID() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = newRequestID()
}
ctx := context.WithValue(r.Context(), requestIDKey, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func RequestIDFromContext(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey).(string)
return id
}
生成 ID:
func newRequestID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
return hex.EncodeToString(b)
}
请求 ID 让日志和排查更方便。context value 要克制使用,适合请求范围元信息,不适合传业务参数。
panic 恢复
func Recover(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if value := recover(); value != nil {
logger.Error("panic recovered",
"panic", value,
"request_id", RequestIDFromContext(r.Context()),
)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
}
恢复 panic 可以避免单个请求把整个服务打崩。但这不是错误处理替代品。可预期失败仍然应该返回 error 并转成 HTTP 响应。panic 更适合真正不可恢复的编程错误。
简单鉴权
func RequireToken(expected string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if token == "" || token != expected {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
这只是入门示例。真实系统要考虑 token 签名、过期、权限范围、密钥轮换和审计日志。但中间件位置是合适的:鉴权通过后才进入业务 handler。
请求日志
func RequestLogger(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info("http request",
"request_id", RequestIDFromContext(r.Context()),
"method", r.Method,
"path", r.URL.Path,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}
}
更完整的版本会记录状态码,需要包装 ResponseWriter。入门阶段先把模式掌握清楚。
小结
Go HTTP 中间件就是 func(http.Handler) http.Handler。通过包装 handler,可以把日志、请求 ID、panic 恢复、鉴权、限流、压缩等横切逻辑从业务 handler 中拿出去。
中间件顺序要明确,context value 要克制,panic 恢复不能替代正常错误处理。用标准库写一次中间件链,你会更理解任何 Go Web 框架的工作方式。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。