中间件函数签名必须是func(http.Handler) http.Handler,需用http.HandlerFunc包裹并转为http.Handler;提前终止必须return;跨中间件传值唯一安全方式是context.WithValue;读取r.Body前必须缓存并重置。

中间件函数签名必须是 func(http.Handler) http.Handler
这是所有可重用中间件的硬性门槛。Go 没有中间件语法糖,所谓“中间件”就是严格符合该签名的函数——参数和返回值都必须是 http.Handler,不能是 http.HandlerFunc 或其他类型。
常见错误包括:
-
func logging(next http.HandlerFunc) http.HandlerFunc:能编译但无法嵌套,下游中间件接收http.Handler,而你传的是具体类型http.HandlerFunc -
func auth(next http.Handler) { ... }:没返回值,http.ListenAndServe会 panic - 返回
http.HandlerFunc却不转成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),必须立刻 return。否则 next.ServeHTTP(w, r) 仍会执行,造成重复响应或 panic。
立即学习“go语言免费学习笔记(深入)”;
典型错误写法:
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
// 缺少 return → 下面这行还会执行!
}
next.ServeHTTP(w, r)
更隐蔽的问题是:在 next.ServeHTTP() 之后再写 http.Error(),此时 header 可能已 flush,触发 http: multiple response.WriteHeader calls panic。
建议封装一个工具函数统一处理:
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)
}
跨中间件传值唯一安全方式是 context.WithValue
不要用全局变量、闭包捕获或自定义 struct 字段传用户信息、请求 ID 等数据。Go 标准库只保证 context.Context 在整个请求生命周期内线程安全且可传递。
示例:在鉴权中间件中注入用户信息:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := getUserFromToken(r.Header.Get("Authorization"))
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
后续中间件或 handler 中获取:
user := r.Context().Value("user").(User)
注意:context.WithValue 的 key 应该是自定义类型(避免字符串冲突),且仅用于传递元数据,不要传大对象或业务逻辑状态。
读取 r.Body 前必须缓存并重置
r.Body 是 io.ReadCloser,只能读一次。日志中间件里用 io.ReadAll(r.Body) 打印后,下游 handler 再读就是空字节——这不是 bug,是设计使然。
要复用 body,必须手动缓存并重置:
body, _ := io.ReadAll(r.Body)
r.Body.Close()
// 重新构造可读 body
r.Body = io.NopCloser(bytes.NewBuffer(body))
// 记录日志或解析 JSON...
log.Printf("body: %s", string(body))
// 后续 handler 仍可正常读取
next.ServeHTTP(w, r)
这个步骤容易被忽略,尤其在调试时发现下游解析失败却查不到原因,大概率是某个中间件偷偷读走了 r.Body。
真正难的不是写一个中间件,而是让它在任意组合、任意顺序、任意 handler 类型(http.ServeMux、chi.Mux、自定义 struct)下都稳定工作——签名、终止、传值、body 处理这四点,漏掉任何一个,都会让“可重用”变成一句空话。


















