Go中责任链最简洁实现是用函数类型切片,通过闭包捕获上下文,以HandlerChain类型封装中间件,显式调用next传递控制权,依赖context.Context共享状态,统一在链首recover处理panic,严格按Recovery→Logging→Timeout→Auth→RateLimit→Metrics→Handler顺序组织中间件。

Go里用函数类型实现责任链最简洁
Go没有类继承,也不鼓励接口泛化,所以责任链不用抽象Handler接口+多层struct嵌套那一套。直接用 func(http.ResponseWriter, *http.Request) error 类型的处理函数切片,配合闭包捕获上下文,是最自然的做法。
典型结构是定义一个 HandlerChain 类型(比如 type HandlerChain []func(http.ResponseWriter, *http.Request) error),再提供 Then 方法追加中间件,ServeHTTP 方法顺序调用并透传控制权。
- 每个中间件函数返回
error表示中断流程(如鉴权失败、参数校验不通过),后续 handler 不再执行 - 必须显式调用
next.ServeHTTP(w, r)或等价的next(w, r)才会进入下一个环节,没有自动“放行”机制 - 注意
*http.Request是可变的:中间件可以修改r.Header、r.Context(),下游能感知到
Context传递是责任链中状态共享的关键
Go的责任链不靠共享字段或全局变量传数据,而是依赖 context.Context。每个中间件应在自己的 ctx 上派生新 context(如 context.WithValue 或 context.WithTimeout),再用 r.WithContext(newCtx) 生成新请求对象传给下一个环节。
常见错误是直接改原 r.Context() 返回的 context——它不可变,WithValue 等操作返回的是新 context,不替换原 request 就等于没传下去。
- 不要在中间件里写
r = r.WithContext(context.WithValue(r.Context(), key, val))后忘记把r传给下一个 handler - 避免用
interface{}作 context key,推荐定义私有未导出类型(如type userIDKey struct{})防止冲突 - 超时、取消、日志 traceID 都应通过 context 逐层向下透传,而不是塞进 map 或全局变量
panic恢复必须在链首统一做,不能分散在每个中间件里
HTTP handler 中一旦 panic,整个 goroutine 会崩溃,导致连接中断且无响应。责任链里若允许任意中间件 panic(比如 JSON 解析失败、空指针解引用),就必须在入口处用 defer/recover 拦截。
正确做法是在链的最外层包装一层 recover handler,比如 RecoveryHandler(Chain.Then(...)).ServeHTTP,而不是让每个中间件自己 defer。
- 分散 recover 会导致错误日志重复、状态不一致(比如日志中间件已记录 start,但 panic 发生在鉴权后,recover 在鉴权里做了,那日志中间件就收不到 end)
- recover 后建议返回
http.StatusInternalServerError并写入简明错误信息(生产环境别暴露堆栈) - 如果用了
http.StripPrefix或自定义http.Handler包装器,确保 recover 层包裹在整个链之外,而非嵌套在某个中间件内部
中间件顺序错位会导致逻辑失效甚至死循环
责任链的执行顺序就是切片索引顺序,但很多开发者误以为“先注册的先执行”,结果把 Logging 放最前、Auth 放最后,导致日志里记了所有请求(包括未认证的非法请求);或者把 Timeout 放太靠后,超时控制根本不起作用。
更隐蔽的问题是中间件自身逻辑引发循环:比如 A 中间件检查 header 里是否有 token,没有就重定向到登录页;B 中间件负责静态文件服务,但没排除 /login 路径,结果重定向又进了 B,B 又没找到文件,再次重定向……最终 302 套娃。
- 典型合理顺序:Recovery → Logging → Timeout → Auth → RateLimit → Metrics → Handler
- 所有中间件都应明确声明“是否处理该请求”以及“是否终止链”,避免隐式 fallback
- 调试时可在每个中间件开头加
log.Printf("[middleware %s] enter", name),看实际执行路径是否符合预期
w.WriteHeader 或 w.Write,客户端就会一直等待直到超时。










