Go中间件函数签名必须是func(http.Handler) http.Handler,否则与标准库及框架不兼容;需显式调用next.ServeHTTP、提前return终止、缓存r.Body、用context.WithValue传值。

Go中间件函数签名必须是 func(http.Handler) http.Handler
不是“可以这样写”,而是标准库和所有主流框架(net/http、gin、chi)都只认这个类型。写成 func(http.HandlerFunc) http.HandlerFunc 看似能跑,但一旦下游用的是 chi.Mux 或自定义 struct 实现的 http.Handler,就会类型不匹配、编译失败或 panic。
常见错误包括:
-
func auth(next http.Handler) { ... }—— 没返回值,http.ListenAndServe直接崩溃 -
func logging(next http.Handler) func(http.ResponseWriter, *http.Request) { ... }—— 返回闭包而非http.Handler,无法接入标准链
正确写法统一用 http.HandlerFunc 包裹再转回 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)
})
}
提前终止请求时必须 return,且不能调用 next.ServeHTTP()
鉴权失败、参数校验不通过、限流触发等场景下,http.Error(w, ...) 后立刻 return 是硬性要求。否则 next.ServeHTTP(w, r) 仍会执行,可能造成:
立即学习“go语言免费学习笔记(深入)”;
- 重复响应(HTTP status 已写,再写 header 触发
http: multiple response.WriteHeader callspanic) - 业务逻辑误执行(比如用户未登录却扣了库存)
- 敏感数据泄露(错误响应后又返回了正常结果)
典型错误写法:
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
// 缺少 return → 下面这行还会执行!
}
next.ServeHTTP(w, r)
建议封装一个安全终止函数:
func writeError(w http.ResponseWriter, status int, msg string) {
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
}
http.Error(w, msg, status)
}
r.Body 只能读一次,日志中间件里读完必须重放
r.Body 是 io.ReadCloser,底层是单次消费的 stream。你在日志中间件里用 io.ReadAll(r.Body) 打印 body 后,下游 handler 再读就是空字节——这不是 bug,是 Go HTTP 的设计契约。
解决方法只有缓存并重置:
- 用
io.TeeReader+bytes.Buffer在读取时同步复制一份 - 或用
httputil.DumpRequest(注意它会自动重放,但会消耗额外内存) - 生产环境慎用完整 body 日志,尤其大文件上传场景
关键点:任何中间件只要读了 r.Body,就必须确保后续 handler 能再次读到相同内容,否则业务逻辑必然出错。
跨中间件传值唯一安全方式是 context.WithValue
不要用全局变量、闭包捕获或 sync.Map 存用户 ID、请求 ID、租户信息等——并发场景下极易出现数据污染或 race condition。
正确姿势是把值注入 r.Context():
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := parseUserID(r.Header.Get("X-User-ID"))
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
下游通过 r.Context().Value("user_id") 获取,类型断言即可。注意:context.Value 只适合传传递元数据,别塞大结构体或函数,避免内存泄漏。
容易被忽略的是:value key 必须是 unexported 类型(比如 type userIDKey struct{}),否则不同包间 key 冲突会导致取值失败——这是线上 debug 时最常踩的坑。


















