go中间件链是函数式闭包嵌套,非注册执行模式;必须返回http.handler、正确调用next.servehttp、用context传递数据、外层recover兜底panic。

中间件链本质是函数套函数,不是“注册+执行”两步走
Go 的 http.Handler 接口只有一个 ServeHTTP 方法,中间件必须靠闭包层层包裹 handler,而不是像 Express 那样先 use() 再 listen()。你写的每个中间件,其实都是返回一个新的 http.Handler,它内部调用下一个 handler —— 这个“链”是构造时就定死的,不是运行时动态调度的。
常见错误现象:panic: http: multiple response.WriteHeader calls,往往是因为中间件里忘了调用 next.ServeHTTP(w, r),或者重复调用了两次;还有人把中间件写成无返回值函数,结果 handler 没被包装,中间件压根没生效。
- 中间件函数签名必须是
func(http.Handler) http.Handler,不是func(http.ResponseWriter, *http.Request) - 链的顺序决定执行顺序:最外层中间件最先拿到请求、最后拿到响应;
log → auth → rateLimit → finalHandler,意味着log是第一个执行、最后一个退出的 - 别在中间件里直接写
w.WriteHeader()或w.Write()后就 return,除非你明确要终止链(比如未授权直接返回 401)
用 net/http 原生方式串链,别依赖第三方库也能很干净
不需要 gorilla/mux 或 chi 就能写出可读性强的中间件链。核心就是用函数式组合:把 handler 传进去,返回新 handler。关键点在于“谁控制 next.ServeHTTP 的调用时机”——它必须出现在你自己的逻辑之后(日志记录完再转发),或之前(鉴权失败提前退出)。
示例:一个带上下文注入的 logger 中间件
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("START %s %s", r.Method, r.URL.Path)
// 注意:必须调用 next,且只能调一次
next.ServeHTTP(w, r)
log.Printf("END %s %s", r.Method, r.URL.Path)
})
}
- 用
http.HandlerFunc转换是为了方便写闭包逻辑,它实现了http.Handler接口 - 不要在闭包里捕获
next外部变量并修改它,next是只读的 handler 实例 - 如果中间件需要改写
*http.Request(比如解析 body、加 header),必须用r.WithContext()创建新请求,原请求不可变
中间件里 panic 会崩掉整个 HTTP server,必须手动 recover
Go 的 http.Server 默认不 recover panic,一个中间件里空指针或越界访问,会导致整个服务挂掉。这不是 bug,是设计如此 —— 你要自己兜底。
常见错误现象:接口偶尔 500,日志里没痕迹,curl 直接断连;其实是 panic 被吞了,server 连 log 都没来得及打。
- recover 必须放在最外层 handler 闭包里,也就是中间件返回的那个
func(w, r)开头 - 别在每个中间件里都写一遍 recover,应该由最外层通用中间件统一处理(比如叫
recovery()) - recover 后别直接
return,要显式写http.Error(w, "Internal Error", http.StatusInternalServerError),否则连接会卡住
简短 recover 示例:
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
log.Printf("PANIC in %s %s: %+v", r.Method, r.URL.Path, err)
}
}()
next.ServeHTTP(w, r)
})
}
Context 传递是唯一靠谱的跨中间件通信方式
别用全局变量、也不要用 map 存 request ID 或用户信息。所有中间件共享的数据,必须通过 r.Context() 传递。Go 的 context.Context 是只读、不可变、线程安全的,而且生命周期和请求一致。
常见错误现象:auth 中间件往 context 写了 userID,但后续中间件取出来是 nil —— 往往因为没用 r = r.WithContext(...) 把新 context 塞回去。
- 写入用
context.WithValue(r.Context(), key, value),key 建议是自定义类型(避免字符串冲突) - 读取用
value := r.Context().Value(key),记得做类型断言 - 别把大对象塞进 context,只放轻量元数据(ID、token、traceID);大结构体建议存在本地变量或 service 层
例如:
type ctxKey string
const userIDKey ctxKey = "user_id"
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := parseUserID(r.Header.Get("Authorization"))
// 注意:必须重新赋值 r!
r = r.WithContext(context.WithValue(r.Context(), userIDKey, id))
next.ServeHTTP(w, r)
})
}
中间件链看着简单,真正难的是 panic 处理时机、context 生命周期、以及哪一层该写 header 哪一层该写 body —— 这些边界一旦错位,问题就藏得深,复现还随机。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











