中间件函数签名必须是func(http.handler) http.handler,因为标准库http.servemux和http.listenandserve只接受http.handler类型,中间件需维持输入输出均为handler的类型契约;若返回其他类型(如func(http.responsewriter, *http.request))将导致编译错误或链断裂,且必须显式调用next.servehttp(w, r)才能延续请求流。

中间件函数签名为什么必须是 func(http.Handler) http.Handler
因为 Go 的 http.ServeHTTP 接口只暴露一个方法,而标准库的 http.ServeMux 和 http.ListenAndServe 都只认 http.Handler 类型。中间件要能“插”进这个流程,就必须维持类型契约:输入是下一个处理器,输出仍是处理器。如果返回其他类型(比如 func(http.ResponseWriter, *http.Request)),就无法被 http.Handle 接收,直接编译报错。
常见错误是写成闭包直接返回 http.HandlerFunc 而没包装 next,导致链断裂。例如:
func brokenMiddleware() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 忘了调用 next.ServeHTTP → 请求到这里就结束了
log.Println("before")
})
}
- 必须显式接收
next http.Handler参数 - 必须在适当位置调用
next.ServeHTTP(w, r),否则后续中间件和业务 handler 不会执行 - 不能用
return提前退出而不调用next,除非你明确想拦截请求(如鉴权失败)
手动嵌套 vs Chain 组合:执行顺序为什么是“后进先出”
Go 中间件链是洋葱模型:最外层中间件最先看到请求、最后看到响应。这源于函数包装的执行逻辑——每个中间件把“下一个 handler”作为参数传入,自己返回的新 handler 包裹它。当你写 logging(Auth(apiHandler)),实际构建的是:
logging → Auth → apiHandler → Auth → logging
所以 Chain 函数必须逆序遍历中间件切片,否则顺序会反:
func Chain(middlewares ...Middleware) Middleware {
return func(final http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- { // 注意这里是倒序
final = middlewares[i](final)
}
return final
}
}
- 正序遍历(
i=0到len-1)会导致apiHandler先被最末尾的中间件包裹,执行时反而最先触发 - 调试时可在每个中间件里打日志,观察
before/after输出顺序验证是否洋葱正确 - 第三方库如
alice或negroni内部也按此逻辑实现,不必重复造轮子,但得懂它怎么工作
中间件里如何安全传递请求上下文数据
不能靠全局变量或闭包捕获,必须用 context.Context 向下透传。标准库的 *http.Request 自带 Context() 方法,且所有中间件拿到的都是同一个 req 实例,所以可以在其中注入值:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
userID, ok := validateToken(token)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// 注入到 context,下游可取
ctx := context.WithValue(r.Context(), "userID", userID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r) // 注意传入修改后的 r
})
}
- 业务 handler 中用
r.Context().Value("userID")取值,类型断言需谨慎 - 推荐定义自定义 key 类型(如
type contextKey string),避免字符串 key 冲突 - 别在中间件里修改
http.ResponseWriter状态码后还调用next.ServeHTTP,容易造成 header 已写入的 panic
为什么中间件里不能直接写 w.WriteHeader 后再调用 next
因为 http.ResponseWriter 一旦写入状态码和 header,底层连接就认为响应头已发送。此时若下游中间件或 handler 再尝试写 header(比如设置 Content-Type),会触发 http: multiple response.WriteHeader calls panic。
正确做法是:要么完全接管响应(不调 next),要么只读/只改 *http.Request,让 next 负责写出。需要修改响应体时,得用 ResponseWriterWrapper 拦截:
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
}
<p>func (w *responseWriterWrapper) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}</p>
- 所有中间件对
w的操作都应是无副作用的,除非你明确在做响应劫持 - 日志中间件记录耗时没问题,但记录响应体长度就得等
next执行完,再从 wrapper 里读缓冲区 - 生产环境建议用
gorilla/handlers这类成熟封装,而不是手写 wrapper,减少边界错误
中间件链看着简单,真正卡住人的地方往往不是怎么写,而是什么时候不该调 next.ServeHTTP、什么时候该换 req.WithContext、以及 WriteHeader 的调用时机——这些点不踩一遍坑,很难真正用稳。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











