Chain Go 的核心是 HandlerFunc 类型而非接口,通过可变参数函数 Chain 组装中间件链,显式调用 next.ServeHTTP 实现传递,中断需避免多次 WriteHeader,改写请求用 context.WithValue,响应修改需 ResponseWriter 装饰器,顺序应为 Recovery→Logging→Auth→RateLimit→Business,高并发下须正确传递和监听 context.Done()。

Chain Go 的核心不是接口,是 HandlerFunc 类型
Go 标准库的 http.Handler 和第三方链式中间件(比如 chi、gorilla/mux)都依赖一个统一的函数签名:func(http.ResponseWriter, *http.Request)。责任链的本质,就是把多个这种函数串起来,每个决定是否继续调用下一个。别一上来就定义 type Chain interface { Handle() }——这反而让链变重、难测试、难组合。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
type HandlerFunc func(http.ResponseWriter, *http.Request)作为基础类型,它本身就能实现http.Handler(只需加个ServeHTTP方法) - 链的组装用可变参数函数,比如
func Chain(hs ...HandlerFunc) HandlerFunc,返回一个闭包,内部按序调用并传入next - 每个中间件里必须显式调用
next.ServeHTTP(w, r)才算“向后传递”,不调就断链——这是最常漏的点
中间件里如何安全地中断或改写请求/响应
常见错误是:在中间件里写了 w.WriteHeader(401) 或 return,但后续 handler 还继续执行,导致 “http: multiple response.WriteHeader calls” 错误。根本原因在于 Go 的 HTTP 处理器没有内置“终止传播”机制,全靠你手动控制流程。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 中断链 = 不调用
next.ServeHTTP(),且确保响应已写出(如w.WriteHeader()+io.WriteString()) - 想改写请求(比如解析 JWT 后注入用户信息),用
r = r.WithContext(context.WithValue(r.Context(), key, value)),下游通过r.Context().Value(key)取 - 想捕获并修改响应体(比如加 JSON 包装),得用
ResponseWriter装饰器(wrapper),重写Write()和WriteHeader(),但注意不要提前触发 write
Chain Go 链顺序错位会导致中间件完全失效
错误现象:日志中间件没打日志、认证中间件没校验、panic 恢复没生效——不是代码写错了,是注册顺序反了。比如把 Recovery() 放在链尾,那前面 panic 就直接炸出 HTTP server,根本轮不到它。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 通用顺序原则:从外到内 ——
Recovery → Logging → Auth → RateLimit → BusinessHandler - 认证类中间件(如
Auth())必须在业务 handler 前,且它内部要检查失败后直接 return,不调 next - 日志中间件建议包住整条链,用 defer 记录耗时和状态码,否则只记开始没记结束
- 用
http.StripPrefix或路由分组时,确保 Chain 是绑定在最终子路由上的,而不是父 mux 上漏掉某一分支
Chain Go 在高并发下要注意 context 超时与 cancel 传递
如果链里某个中间件(比如 DB 查询)没读取 r.Context().Done(),或者没把 cancel 传给下游协程,整个请求就卡死,连接堆积,CPU 空转。这不是链模式的问题,是 Go context 使用惯性缺失。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 所有阻塞操作(
db.QueryContext()、http.Client.Do(req.WithContext()))必须传入r.Context() - 自己启 goroutine 时,用
ctx, cancel := context.WithTimeout(r.Context(), time.Second*5),并在 defer 里cancel() - 别在中间件里用
context.Background(),那是放弃超时控制的信号 - 链中任意一环调用
cancel(),整个链的 context 都会关闭,下游需监听<code>select { case
链本身不复杂,复杂的是每个环节怎么和 context、error、response writer 协同。最容易被忽略的,是中间件之间没有共享状态容器,所有数据只能靠 context.Value 或显式参数传递——而前者容易滥用,后者容易漏传。










