中间件函数必须返回http.handler,正确签名是func(http.handler) http.handler;鉴权失败须显式return防止下游执行;中间件顺序影响拦截范围,推荐recovery→logging→auth→ratelimit→handler;读取r.body前需缓存以避免下游读空。

中间件函数必须返回 http.Handler
不返回 http.Handler 的函数根本不能叫中间件,http.ListenAndServe 会直接 panic 或静默失败。常见错误是写成 func auth(next http.Handler) { ... }——没返回值,链就断了;或者返回 func(http.ResponseWriter, *http.Request) 却没转成 http.HandlerFunc,类型不匹配导致编译报错或运行时 panic。
正确签名只有一种核心形式:func(http.Handler) http.Handler。最轻量的实现方式是用 http.HandlerFunc 包一层闭包:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return // 必须显式 return,否则 next.ServeHTTP 仍会执行
}
next.ServeHTTP(w, r)
})
}
注意:哪怕下游 handler 是自定义 struct 实现的 http.Handler(比如实现了 ServeHTTP 方法),中间件也必须接收 http.Handler 接口,不能限定为 http.HandlerFunc。
鉴权失败时必须提前 return,且不能调用 next.ServeHTTP
鉴权逻辑一旦失败,就得立刻写响应并退出,否则请求会继续往下走,可能触发业务逻辑、修改数据库,甚至泄露敏感数据。
-
if token == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) }—— 少了return,后面next.ServeHTTP还会执行,这是高频 bug - 所有提前终止路径(如 token 过期、签名无效、IP 不在白名单)都必须包含
return - 别在
next.ServeHTTP(w, r)之后再调用http.Error或w.WriteHeader,此时 header 很可能已 flush,会 panic 报http: multiple response.WriteHeader calls
如果需要统一错误格式,建议封装一个工具函数:writeError(w, err),内部先检查 w.Header().Get("Content-Type") 是否为空再决定是否写 header。
中间件顺序决定拦截生效范围
Go 中间件是洋葱模型:先注册的在外层,后注册的在内层。顺序错了,鉴权就可能失效,甚至引发 panic。
- 推荐顺序(由外到内):
Recovery→Logging→Auth→RateLimit→Handler - 把
Auth放最里层?那日志、限流、甚至 panic 恢复都发生在鉴权之前,完全失去意义 - 把
Auth放最外层?它能拦住所有非法请求,但也会让未认证请求被记日志——这有时是合理设计,有时是泄露行为痕迹
实际使用时链式包裹:
handler := http.HandlerFunc(yourHandler)
handler = recoveryMiddleware(handler)
handler = loggingMiddleware(handler)
handler = authMiddleware(handler)
handler = rateLimitMiddleware(handler)
http.ListenAndServe(":8080", handler)
读取 r.Body 前必须缓存,否则下游拿不到数据
HTTP body 是 io.ReadCloser,标准库只允许读一次。如果鉴权中间件里调用了 io.ReadAll(r.Body)(比如解析 JWT payload 或校验签名),下游 handler 再读就是空内容,常导致 JSON 解析失败或参数丢失。
解决方法是重放 body:
- 用
io.NopCloser+bytes.NewBuffer缓存原始 body - 调用
r = r.Clone(r.Context())创建新请求,并替换r.Body - 注意:修改
r.URL或其他字段后也必须r = r.Clone(r.Context()),否则可能 panic
这个点容易被忽略——因为不报错,只是下游逻辑静默失败。调试时若发现业务 handler 收不到 POST 数据,优先查鉴权中间件有没有偷偷读过 r.Body。











