Go中间件必须统一签名func(http.Handler) http.Handler,显式调用next.ServeHTTP(),注册时用http.Handle而非http.HandleFunc,recover中间件须置最外层并包装ResponseWriter防重复写头。

Go 语言没有“中间件”这个内置类型,所有中间件都是基于 http.Handler 接口的包装函数——写错类型签名、漏掉 ServeHTTP 调用、或误用 http.HandleFunc 注册链式中间件,是新手踩坑最集中的三个地方。
中间件函数必须返回 http.Handler,不能直接返回 http.HandlerFunc
常见错误是写成:func logging(next http.HandlerFunc) http.HandlerFunc。这看似能编译,但一旦嵌套多个中间件(比如日志 + 鉴权),就会在注册时触发类型不匹配:下游中间件期望接收 http.Handler,而你传的是 http.HandlerFunc(虽然后者实现了接口,但类型不同,Go 不自动转换)。
正确写法统一用 http.Handler 作为参数和返回值:
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("→ %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("← %s %s", r.Method, r.URL.Path)
})
}
- 所有中间件函数签名保持一致,才方便链式组合
-
http.HandlerFunc是工具类型,仅用于把普通函数转成http.Handler;它本身不是“中间件类型” - 别在中间件里做
return早退——必须显式调用next.ServeHTTP()或手动写响应,否则请求就卡住了
注册中间件必须用 http.Handle,不能用 http.HandleFunc
http.HandleFunc 只接受 func(http.ResponseWriter, *http.Request),它内部会帮你包一层 http.HandlerFunc,但无法接入已包装好的中间件链。如果你写:http.HandleFunc("/api", auth(logging(handler))),会编译报错:cannot use ... as func(http.ResponseWriter, *http.Request)。
立即学习“go语言免费学习笔记(深入)”;
正确注册方式是先将业务处理器转为 http.HandlerFunc,再逐层套中间件,最后交给 http.Handle:
handler := http.HandlerFunc(userHandler)
http.Handle("/api/user", auth(logging(handler)))
-
http.Handle接收的是http.Handler接口值,天然支持任意包装层级 - 如果用
http.NewServeMux(),也必须用srvMux.Handle,而非srvMux.HandleFunc - 第三方路由库(如
gorilla/mux)同理:用router.Use()或router.Handler(),不是router.HandleFunc()
panic 恢复中间件必须检查响应头是否已写
Recovery 中间件里直接调用 http.Error() 很危险:如果下游 handler 已经调用过 w.WriteHeader(200) 或写入了 body,再调 http.Error() 会 panic:“http: multiple response.WriteHeader calls”。
安全做法是用包装器检测状态,或手动控制写入:
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 包装 ResponseWriter,捕获 WriteHeader 调用
rw := &responseWriter{w: w}
defer func() {
if err := recover(); err != nil {
if !rw.wroteHeader {
w.WriteHeader(http.StatusInternalServerError)
}
io.WriteString(w, "Internal Server Error")
}
}()
next.ServeHTTP(rw, r)
})
}
- 标准库不提供可检测的
ResponseWriter,得自己封装字段记录wroteHeader - 别依赖
w.Header().Get("Content-Type")判断——header 可能为空但状态码已发 - 更稳妥的做法是:所有中间件统一用包装后的
ResponseWriter,避免下游直接操作原始w
真正难的不是写一个中间件,而是让多个中间件共享状态又不互相污染——比如鉴权中间件把用户信息塞进 r.Context(),下游 handler 才能安全取用;一旦有人直接改 r.URL.Path 又不 deep-copy,后续中间件看到的就是脏数据。这些细节不调试到线上 500 错误,很难意识到。


















