Go中用func(http.Handler) http.Handler签名实现中间件链,先注册的在外层,需显式调用next.ServeHTTP(w, r);推荐切片倒序循环组装,避免嵌套过深和顺序错误。

Go 里怎么写一个能串起来的中间件链
Go 没有内置的责任链类型,但用函数签名 func(http.Handler) http.Handler 就能自然形成链式调用。关键不是“模拟设计模式”,而是让每个中间件只关心自己该做的事,把下一个处理器当参数传进去。
常见错误是中间件里忘了调用 next.ServeHTTP,结果请求直接卡住,没报错也没响应;或者误在中间件里多次调用,导致 handler 被执行两遍。
- 每个中间件必须接收一个
http.Handler并返回一个新的http.Handler - 内部要显式调用
next.ServeHTTP(w, r),否则链就断了 - 顺序很重要:先注册的中间件最外层,后注册的更靠近业务 handler
为什么用 func(http.Handler) http.Handler 而不是 interface
Go 标准库的 http.Handler 本身就是一个接口,但中间件用函数类型更轻量、更灵活。用 interface 容易过度抽象,反而让链的组装变复杂,还可能引入不必要的类型断言或包装结构体。
性能上,函数闭包比 interface 动态调用少一层间接跳转;兼容性上,所有标准库函数(比如 http.StripPrefix、http.TimeoutHandler)都适配这个签名,能直接塞进链里。
立即学习“go语言免费学习笔记(深入)”;
-
func(h http.Handler) http.Handler是事实标准,社区工具(如chi、gorilla/mux中间件)都按这个来 - 如果硬套 interface,会逼你写一堆
type AuthMiddleware struct{ Next http.Handler }这类冗余 wrapper - 闭包天然携带上下文(比如配置、logger),不用额外传参或依赖全局变量
panic 恢复和日志中间件怎么安全插入链中
这类中间件必须放在链靠前位置,否则 panic 会绕过它们直接崩掉整个 goroutine。但要注意:recover 只对当前 goroutine 有效,且只能捕获同步 panic —— 如果你在 goroutine 里启了个异步任务并 panic,主链收不到。
日志中间件也容易踩坑:如果在 defer 里记录耗时,但 handler 写了 header 后 panic,w.Header().Get("Content-Length") 可能拿不到真实值,得用 ResponseWriter 包装器劫持 WriteHeader 和 Write 调用。
- panic 恢复中间件应放在最外层,即第一个被调用的位置
- 不要在中间件里直接
log.Fatal或os.Exit,那会杀掉整个进程,不是中断当前请求 - 记录响应状态码要用包装过的
ResponseWriter,标准http.ResponseWriter不暴露已写状态
实际组装链时怎么避免嵌套过深或顺序错乱
手写 mw1(mw2(mw3(h))) 看着就晕,而且加个中间件就得改一长串。用切片 + 循环反向组装更清晰:从最后一个中间件开始,逐个包住前一个,最后得到顶层 handler。
但要注意,循环组装时别把 handler 当成值传递再修改 —— http.Handler 是接口,底层可能是指针,误操作会导致 nil panic。另外,某些中间件(比如带超时的)需要访问原始 *http.Request,不能被其他中间件提前消费 body。
- 推荐用 slice 存中间件函数,然后用 for 循环倒序构建:
for i := len(mws)-1; i >= 0; i-- { h = mws[i](h) } - 读取
r.Body的中间件(如 JWT 解析、JSON 解析)要尽量靠前,否则后面中间件可能已读过一次,body 变空 - 不要在中间件里修改
r.URL.Path后不调用r.URL.Opaque = "",否则后续路由匹配可能出错
责任链真正的复杂点不在写法,而在中间件之间的隐式依赖:A 依赖 B 已设置 context key,B 依赖 C 已校验权限。这种顺序耦合没法靠类型系统检查,只能靠文档和测试覆盖。










