Go HTTP中间件必须统一为func(http.Handler) http.Handler签名,本质是装饰器;需确保类型兼容、正确调用next.ServeHTTP、避免ResponseWriter冲突,并用切片链式组装以提升可维护性。

中间件函数签名必须统一为 func(http.Handler) http.Handler
Go 的 HTTP 中间件本质是装饰器,核心约束是类型兼容:每个中间件必须接收一个 http.Handler 并返回一个新的 http.Handler。不满足这个签名的函数(比如直接接收 *http.Request 和 http.ResponseWriter)无法串成链,会编译报错 cannot use xxx (type func(http.ResponseWriter, *http.Request)) as type http.Handler。
常见错误是写成「handler 内联式」:
func logging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL.Path)
next(w, r) // ❌ 这里 next 是 func,不是 Handler
}
}正确做法是让入参和返回值都保持 http.Handler 类型,或统一用 http.HandlerFunc —— 因为它实现了 http.Handler 接口:
- 推荐写法:参数和返回值都用
http.Handler,最正交,适配任意 handler 类型(包括自定义 struct) - 实用写法:参数用
http.HandlerFunc,返回值转成http.Handler(通过http.HandlerFunc(fn)转换),适合快速开发
链式调用顺序决定执行时机:外层中间件先执行 Before,后执行 After
中间件链不是并行叠加,而是嵌套闭包。假设有 mwA → mwB → handler,实际构造出的是:mwA(mwB(handler))。这意味着:
立即学习“go语言免费学习笔记(深入)”;
- 请求阶段:
mwA的前置逻辑 →mwB的前置逻辑 →handler.ServeHTTP→mwB的后置逻辑 →mwA的后置逻辑 - 任何中间件里没调用
next.ServeHTTP(w, r),后续就彻底中断(比如鉴权失败直接写 401) - panic 捕获类中间件(如 recover)必须放在最外层,否则内层 panic 会穿透出去
示例中,日志中间件如果想记录完整耗时,必须在调用 next.ServeHTTP 前后都打点:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // ✅ 必须显式调用
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}避免在中间件里修改 ResponseWriter 导致 write after WriteHeader
多个中间件共享同一个 http.ResponseWriter,一旦某个中间件提前调用 w.WriteHeader(401) 或 w.Write([]byte{...}),后续中间件再写就会触发 http: superfluous response.WriteHeader call 或更隐蔽的 write after WriteHeader panic。
安全做法是包装 ResponseWriter,拦截写操作做判断:
- 用
httptest.NewRecorder()仅用于测试,不能用于生产 - 生产环境应实现轻量 wrapper,例如检查
Written状态、缓存 header/body、支持 hijack - 第三方库如
go-chi/chi的middleware.WithValue或gorilla/handlers.CompressHandler内部已处理该问题,可参考其实现
简单 wrapper 示例(省略 error 处理):
type responseWriterWrapper struct {
http.ResponseWriter
written bool
}
func (w *responseWriterWrapper) WriteHeader(statusCode int) {
if !w.written {
w.ResponseWriter.WriteHeader(statusCode)
w.written = true
}
}
func (w *responseWriterWrapper) Write(b []byte) (int, error) {
if !w.written {
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(b)
}不要把中间件链硬编码进 main(),用切片 + 循环组装更灵活
手写 mwA(mwB(mwC(handler))) 看似清晰,但新增/删减中间件要改多处,且无法动态启用(比如 dev 环境加 pprof,prod 关闭)。更可持续的方式是用 slice 存储中间件函数,再用循环组合:
func chain(handlers ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
return func(final http.Handler) http.Handler {
for i := len(handlers) - 1; i >= 0; i-- {
final = handlers[i](final)
}
return final
}
}
// 使用
handler := chain(logging, auth, metrics)(http.HandlerFunc(yourHandler))注意顺序:slice 里从左到右声明,但组合时需倒序遍历(即最后一个中间件最先包装),才能保证执行顺序符合直觉(logging 最外层,yourHandler 最内层)。
容易忽略的一点:中间件本身可能依赖初始化(如读配置、连 DB),别在 chain() 里直接调用,应在 main() 初始化好再传入闭包。

















