go中间件必须用函数链而非单个handler,因http生命周期需多阶段干预且http.handler仅提供单一入口;硬塞逻辑导致职责混杂、复用难、测试爆炸,而责任链通过func(http.handler) http.handler封装顺序执行与中断能力。

Go 中间件为什么必须用函数链而不是单个 handler?
因为 HTTP 请求生命周期需要多阶段干预,而 http.Handler 接口只允许一个 ServeHTTP 入口。硬塞所有逻辑进去会导致职责混杂、复用困难、测试爆炸。
责任链本质是把「顺序执行 + 可中断」封装成函数签名:func(http.Handler) http.Handler。每个中间件接收下一个 handler,返回新 handler,在调用 next.ServeHTTP 前后插入逻辑。
- 不调用
next.ServeHTTP→ 链路终止(如鉴权失败直接写 401) - 在
next.ServeHTTP前操作 → 请求预处理(如解析 Token、设置 context.Value) - 在
next.ServeHTTP后操作 → 响应后处理(如记录耗时、加 header) - 错误不能 panic,必须显式处理并决定是否继续链路(比如日志中间件不该因下游 panic 而丢日志)
ctx.Value 传参 vs 中间件闭包变量:哪个更安全?
用 context.WithValue 是唯一可靠方式。闭包捕获的变量在并发请求中会互相污染——Go 的中间件函数在启动时就构造完毕,所有请求共享同一份闭包变量。
典型错误写法:func(auth String) Middleware { return func(next http.Handler) http.Handler { ... use auth ... } } —— 这里的 auth 是闭包变量,但多个请求共用同一个 auth 值,毫无意义。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:从
*http.Request解析数据,存入req.Context(),下游用ctx.Value(key)取 - key 必须是自定义类型(如
type userIDKey Struct{}),避免字符串 key 冲突 - 不要把大结构体塞进 context,只放轻量标识(ID、token 字符串、traceID)
- context 传递是单向的,下游无法改写上游塞的值
panic 恢复中间件为什么必须放在链最外层?
因为 Go 的 defer/recover 只对当前 goroutine 生效,且只能捕获本函数内 panic。如果 recovery 中间件不在链首,它根本看不到下游中间件或业务 handler 抛出的 panic。
常见错误是把它插在 logger 或 auth 后面,结果 panic 仍向上冒泡到 http.Server 默认处理逻辑,返回 500 且无日志。
- 注册顺序必须是:
recovery → logger → auth → metrics → yourHandler - recover 后要主动写响应(
w.WriteHeader(500)+w.Write(...)),否则连接可能卡住 - 别在 recover 里调用
log.Fatal或os.Exit,会杀掉整个服务 - recover 不该吞掉所有 panic,比如内存溢出类 panic 就不该恢复
中间件顺序错乱导致 context 覆盖或 header 丢失
顺序不是随便排的。比如 gzip 中间件必须在所有修改响应体的中间件之后(如模板渲染),否则压缩的是未渲染完的空 body;而 cors 必须在所有可能写 header 的中间件之前,否则被覆盖。
另一个高频坑是:两个中间件都往 context 写同名 key,后写的覆盖前写的,下游取到的永远是最后一个值。
- 按生命周期分层:输入解析(body/json)→ 认证授权 → 上下文注入(user, tenant)→ 业务处理 → 输出包装(gzip, cors)
- 修改 response header 的中间件(如
CORS、SecurityHeaders)必须靠近链尾 - 读取 request body 的中间件必须在任何可能 consume body 的 handler 之前(
req.Body只能读一次) - 用
httputil.DumpRequestOut和DumpResponse在关键节点打日志,验证 header/body 是否符合预期
责任链看着简单,真正难的是各中间件之间的隐式契约:谁读 context、谁写 context、谁消费 body、谁写 header、谁可能 panic。这些细节不厘清,加再多中间件也只是埋雷。