中间件函数签名必须返回 http.Handler 或 http.HandlerFunc;错误签名导致编译失败或 panic;需显式 return 终止请求;r.Body 只能读一次,需手动缓存;中间件按洋葱模型嵌套,顺序关键;跨层传值用 context.WithValue。

中间件函数签名必须返回 http.Handler
Go 没有中间件语法糖,它只是函数套函数:输入一个 http.Handler,输出另一个 http.Handler。写错签名会导致编译失败或运行时 panic。
-
func auth(next http.Handler) { }—— 没返回值,http.ListenAndServe收不到有效 handler -
func auth(next http.Handler) func(http.ResponseWriter, *http.Request) { }—— 类型不匹配,无法嵌套进链路 - 正确写法只能是
func(http.Handler) http.Handler或func(http.HandlerFunc) http.HandlerFunc - 用
http.HandlerFunc包裹闭包是最轻量、最常用的方式,比如:return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... }) - 如果下游是自定义 struct 实现的
http.Handler(如实现了ServeHTTP方法),中间件也得接收http.Handler接口,不能只认http.HandlerFunc
拦截请求后必须显式 return,否则请求会继续往下走
鉴权失败、参数校验不通过、限流触发——这些场景下你得主动写响应并退出,否则逻辑错乱甚至数据泄露。
- 错误写法:
if token == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) }—— 少了return,后面next.ServeHTTP()仍会执行 - 所有提前终止路径都必须显式
return - 别在
next.ServeHTTP()之后再调http.Error(),此时 header 可能已 flush,会 panic 报http: multiple response.WriteHeader calls - 建议封装一个
writeError(w, err)工具函数,内部先检查w.Header().Get("Content-Type")是否为空再决定是否写 header
r.Body 只能读一次,日志中间件想打请求体就得缓存
标准库的 r.Body 是 io.ReadCloser,一旦被 io.ReadAll(r.Body) 或 json.NewDecoder(r.Body).Decode() 消费,下游 handler 再读就是空字节。
- 调试时想看完整 body,必须手动缓存:
bodyBytes, _ := io.ReadAll(r.Body),再r.Body = io.NopCloser(bytes.NewReader(bodyBytes))塞回去 - 生产环境慎用,大文件或高并发下易 OOM;建议只采样或只记录长度
r.ContentLength - 别用
r.ParseForm()后再去读r.Body,它内部已经读过一遍了 - 框架如 Gin 默认做了 Body 缓存,但原生 net/http 没有,这点容易被忽略
多个中间件顺序嵌套时,next 指向的是“下一层”而非最终 handler
中间件是洋葱模型:最外层中间件的 next 是第二层,第二层的 next 是第三层……最内层的 next 才是最终路由处理器(比如 http.HandlerFunc)。
立即学习“go语言免费学习笔记(深入)”;
- 手写嵌套必须显式拼接:
loggingMiddleware(authMiddleware(handler)),不是靠注册自动生效 - 顺序颠倒会导致逻辑错乱,比如认证放到了日志之后,但认证失败时没日志
- 若需跨中间件传递数据(如用户 ID),必须靠
context.WithValue透传,且 key 类型要统一(推荐私有 struct 字段而非字符串) - 用结构体封装可配置中间件(如带超时、开关的日志)比闭包更清晰,尤其当需要组合或动态配置时


















