中间件链本质是handlerfunc层层包装,签名必须为func(http.handler) http.handler,需显式调用next.servehttp(w, r),顺序决定执行流(外层先执行、内层后执行),用倒序组合实现chain(m1,m2,m3)(h)等价于m1(m2(m3(h)))。

中间件链的本质是 HandlerFunc 的层层包装
Go 的 http.Handler 接口只有一个 ServeHTTP 方法,中间件就是满足这个接口的“包装器”:它接收一个 http.Handler,返回另一个 http.Handler。关键不在于“链”的形式,而在于调用顺序——外层中间件先执行逻辑,再决定是否调用 next.ServeHTTP(即内层 handler)。
常见错误是忘记调用 next.ServeHTTP,导致请求卡死;或在调用前写了 w.WriteHeader 后又试图写 body,触发 http: multiple response.WriteHeader calls 错误。
- 中间件函数签名应统一为
func(http.Handler) http.Handler,便于组合 - 使用
http.HandlerFunc转换闭包为 handler,避免手动实现接口 - 顺序很重要:
logging(middleware(auth(handler)))表示先 log,再 auth,最后业务 handler
用函数式组合构建可复用中间件链
不要手写嵌套调用,用组合函数让代码更清晰、易测、可复用。最简组合方式是定义 func Chain(handlers ...func(http.Handler) http.Handler) func(http.Handler) http.Handler:
func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
return func(final http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
final = middlewares[i](final)
}
return final
}
}
注意倒序遍历:要让第一个 middleware 包裹最外层,就得从右往左套。比如 Chain(m1, m2, m3)(h) 等价于 m1(m2(m3(h)))。
- 每个中间件内部必须显式调用
next.ServeHTTP(w, r),否则请求中断 - 若中间件需提前终止(如鉴权失败),应直接写响应并 return,不再调用 next
- 避免在中间件里修改
*http.Request.URL或*http.Request.Header后不 deep-copy,影响下游 handler
Context 传递数据比全局变量或字段赋值更安全
中间件之间传参(如用户 ID、请求 ID)不能靠给 http.Request 加字段(Go 1.19+ 已禁止),必须用 context.WithValue 并在 handler 中通过 r.Context() 取值。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
典型错误是把 context key 定义为 string 类型,导致跨包冲突;应定义为未导出的私有类型:
type contextKey string
const userIDKey contextKey = "user_id"
<p>func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := parseToken(r.Header.Get("Authorization"))
ctx := context.WithValue(r.Context(), userIDKey, id)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}</p><p>func ProfileHandler(w http.ResponseWriter, r *http.Request) {
id := r.Context().Value(userIDKey).(string) // 类型断言需谨慎
// ...
}</p>
- key 类型必须是自定义类型,防止与其他包 key 冲突
- value 尽量用不可变结构体或基本类型;避免传指针或 map,引发并发读写 panic
- handler 中取值前务必做类型检查(如用
if id, ok := ctx.Value(userIDKey).(string); ok)
调试中间件链时优先检查 ServeHTTP 是否被跳过或重复调用
最常见的运行时问题不是语法错,而是逻辑漏:比如日志中间件写了 log.Println("before") 却没写 next.ServeHTTP,或者 panic 恢复中间件没调用 recover() 导致整个链崩溃。
建议在每个中间件开头加 debug log,格式统一为 [middleware-name] enter,结尾加 [middleware-name] exit,再配合 http.StripPrefix 或路由调试,能快速定位哪一层断了。
- 用
http.TimeoutHandler包裹整条链时,注意它本身是个中间件,需放在最外层 - 如果用了第三方路由器(如
gorilla/mux),它的.Use()方法本质也是 Chain,但顺序和你手写的可能不一致,需看文档确认包裹时机 - 生产环境禁用
panic直出堆栈,中间件里一定要有defer func() { if r := recover(); r != nil { /* 记录并返回 500 */ } }()
真正难的不是写一个中间件,而是让多个中间件在共享 context、处理 error、控制流和资源释放上互不干扰。每加一层,都要问:它会不会阻塞后续?会不会污染 context?有没有清理副作用?
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










