Go中间件必须严格遵循func(http.Handler) http.Handler签名,显式调用next.ServeHTTP(w, r),并通过context.Context传递数据,否则会导致静默失效或panic。

Go 中多层嵌套中间件不是靠“自动链式调度”,而是靠函数签名一致性和手动调用顺序严格控制的。写错签名、漏掉 next.ServeHTTP、传参方式不当,三者任一都会导致中间件静默失效或 panic。
中间件函数签名必须是 func(http.Handler) http.Handler
这是所有中间件能被组合的前提。任何偏离这个签名的写法(比如 func(http.HandlerFunc)、func(w http.ResponseWriter, r *http.Request) 或带额外参数如 func(h http.Handler, timeout time.Duration))都无法参与标准链式拼接。
- 错误示例:
func(authHandler http.HandlerFunc, logger *log.Logger) http.HandlerFunc—— 返回值类型不对,无法嵌套 - 正确做法:用闭包捕获配置,内层返回标准签名:
func(logger *log.Logger) func(http.Handler) http.Handler - 框架如 Gin/Echo 依赖这个契约识别中间件;
http.ListenAndServe也只接受http.Handler类型
next.ServeHTTP(w, r) 的位置决定中间件语义
洋葱模型不是魔法,是你在中间件里写哪一行代码决定的。同一份中间件逻辑,next.ServeHTTP 放前、放中、放后,行为完全不同。
- 放在开头:前置拦截(如鉴权失败直接
http.Error(w, "forbidden", 403)并 return) - 放在中间:改写请求后放行(如解析 body 后替换
r.Body,再调用next.ServeHTTP) - 放在末尾:后置处理(如记录耗时、设置
w.Header().Set("X-Processed-At", ...)),但注意此时响应可能已写出,w.Header().Set()会 panic - 完全不调用:终止流程(如 CORS 预检直接
w.WriteHeader(200)后 return)
跨中间件传数据只能靠 context.Context
别给 *http.Request 加字段(Go 1.19+ 已禁止),也别用闭包变量或全局变量存 request-scoped 数据——HTTP server 是并发的,必然数据竞争。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 写值:
ctx := r.Context().WithValue(myKey{}, value),其中myKey{}必须是自定义空 struct 类型,避免字符串 key 冲突 - 读值:
value, ok := r.Context().Value(myKey{}).(string),务必做非空判断和类型断言 - 传下去:
r = r.WithContext(ctx),再调用next.ServeHTTP(w, r) - 别用
context.Background()替代r.Context()——它没有 cancel/timeout 信号,也无法传递 request 生命周期信息
组合多个中间件时顺序极易出错
手写 m1(m2(m3(handler))) 容易把包裹方向搞反;用 pipeline 组合时,也要注意 fold 方向:从右往左套才符合“外层先执行”的直觉。
-
Chain(m1, m2, m3).Then(h)等价于m1(m2(m3(h))),即请求流为:m1 → m2 → m3 → h → m3 → m2 → m1 - 调试技巧:每个中间件开头加
log.Printf("→ %s", "m1"),结尾加log.Printf("← %s", "m1"),看日志是否成对出现 - panic 捕获中间件必须放在最外层,否则压根 catch 不到;修改 body 的中间件必须在依赖它的中间件之前,否则下游读不到原始内容
真正容易被忽略的是:中间件之间没有隐式通信,所有共享状态都必须显式通过 context 传递;而 next.ServeHTTP 是否被调用、何时被调用,直接决定了整个请求生命周期是否完整——它不是可选步骤,是中间件存在的前提。

















