http中间件本质是接收并返回http.handler的嵌套包装函数,基于接口和闭包实现;需确保调用next.servehttp()且不重复写响应;日志中间件读r.body前须缓存,多中间件按洋葱模型顺序执行。

HTTP中间件的本质是http.Handler的嵌套包装
Go 的 HTTP 中间件不是框架内置的概念,而是基于 http.Handler 接口和闭包组合实现的函数链。你写的每个“中间件”,其实就是一个接收 http.Handler 并返回新 http.Handler 的函数。
常见错误是直接在中间件里调用 w.WriteHeader() 或 w.Write() 后忘记调用 next.ServeHTTP(),导致后续处理被跳过;或者在调用 next.ServeHTTP() 后又尝试写响应,触发 http: multiple response.WriteHeader calls 错误。
- 中间件必须接收一个
http.Handler参数,并返回一个新的http.Handler - 实际业务逻辑要放在
next.ServeHTTP(w, r)调用前后,而不是替代它 - 日志类中间件通常只读请求头、URL、方法,不读
r.Body(否则会消耗流,下游拿不到)
用http.HandlerFunc快速构造可链式调用的中间件
手动实现 http.Handler 接口太重,99% 场景用 http.HandlerFunc 就够了——它本身就是 func(http.ResponseWriter, *http.Request) 类型,且自带 ServeHTTP 方法。
比如一个基础日志中间件:
立即学习“go语言免费学习笔记(深入)”;
func loggingMiddleware(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.ServeHTTP(w, r)
log.Printf("END %s %s", r.Method, r.URL.Path)
})
}
注意:这里没动 w,只是透传。如果想加响应头或修改状态码,得用包装器(如 responseWriter 适配),不能直接改原生 http.ResponseWriter。
为什么r.Body不能重复读?日志中间件如何安全记录请求体
HTTP 请求体是单次读取的流(io.ReadCloser),一旦被 json.NewDecoder(r.Body).Decode() 消费,后续再读就是空的。日志中间件若想打请求体内容,必须先 io.ReadAll(r.Body),再用 io.NopCloser() 造个新 Body 塞回去。
- 仅在调试或低流量场景记录完整
Body,生产环境建议只记长度或采样 - 别用
r.ParseForm()后再读r.Body,它内部已消费一次 - 如果用了
gin或echo等框架,它们默认做了Body缓存,但标准库没有
多个中间件顺序执行时,next指向谁?
中间件是“洋葱模型”:最外层中间件的 next 是第二层,第二层的 next 是第三层……最内层的 next 才是最终路由处理器(比如 http.HandlerFunc)。顺序错了,日志就可能漏掉某些环节,或者 panic 发生在中间件外无法捕获。
典型注册方式:
handler := loggingMiddleware(
recoverMiddleware(
authMiddleware(
http.HandlerFunc(yourHandler),
),
),
)
这个顺序意味着:请求进来先过 loggingMiddleware,最后才进 yourHandler;响应返回时则逆序——yourHandler 写完,authMiddleware 可能加 header,recoverMiddleware 捕获 panic,loggingMiddleware 才打结束日志。
容易忽略的是:中间件之间没有共享上下文,想传数据得用 r.Context().WithValue(),且记得用自定义 key 类型防冲突。










