中间件函数签名必须是func(http.Handler) http.Handler,接收并返回http.Handler,显式调用next.ServeHTTP(w, r),提前终止需return防重复响应,跨中间件传值唯一安全方式是context.WithValue。

中间件函数签名必须是 func(http.Handler) http.Handler
Go 没有中间件语法糖,所谓“中间件”就是接收一个 http.Handler、返回另一个 http.Handler 的函数。写成 func(http.HandlerFunc) http.HandlerFunc 虽能编译,但会卡死在链式调用上——下游中间件期望 http.Handler,你传的是 http.HandlerFunc(类型不匹配,Go 不自动转换)。
常见错误包括:
-
func auth(next http.Handler) { }—— 没返回值,http.ListenAndServe直接 panic -
func auth(next http.Handler) func(http.ResponseWriter, *http.Request) { }—— 类型不匹配,无法嵌套进标准 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语言免费学习笔记(深入)”;
典型错误写法:
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
// 缺少 return → 下面这行还会执行!
}
next.ServeHTTP(w, r)
更隐蔽的问题是:在 next.ServeHTTP() 之后再写 http.Error(),此时 response header 可能已 flush,触发 http: multiple response.WriteHeader call 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)
}
读取 r.Body 前必须缓存,否则下游收不到数据
r.Body 是 io.ReadCloser,只能读一次。日志中间件里用 io.ReadAll(r.Body) 打印 body 后,下游 handler 再读就是空字节——这不是 bug,是设计使然。
要复用 body,得手动缓存并重置:
body, _ := io.ReadAll(r.Body) r.Body.Close() // 重新构造可复用的 Body r.Body = io.NopCloser(bytes.NewReader(body)) // 记录日志或校验逻辑... next.ServeHTTP(w, r)
注意:bytes.NewReader(body) 返回的是 io.Reader,需用 io.NopCloser 包一层才能满足 io.ReadCloser 接口。
跨中间件传值唯一安全方式是 context.WithValue
不要用全局变量、闭包变量或自定义 struct 字段传用户 ID、token、请求 ID 等数据——并发下会错乱或覆盖。
正确做法是基于请求上下文传递:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
// ... 验证逻辑
ctx := context.WithValue(r.Context(), "user_id", "12345")
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
后续中间件或 handler 中通过 r.Context().Value("user_id") 获取,类型断言后使用。
容易被忽略的一点:key 不该用字符串字面量,应定义为 unexported 类型(如 type ctxKey string),避免不同中间件 key 冲突。


















