Go中间件必须显式包装handler,正确形式为func(next http.Handler) http.Handler;漏return、类型错、未调next.ServeHTTP()或c.Abort()后不return均致逻辑绕过。

Go 没有“注册即拦截”的魔法机制,所有中间件必须显式包装 handler 才生效;漏掉返回 http.Handler、忘记 return 或没调 next.ServeHTTP(),请求就完全绕过你的逻辑。
中间件函数必须返回 http.Handler
签名错一个字符都可能编译失败或静默失效。常见错误写法:func auth(next http.Handler) { }(无返回)、func auth(next http.Handler) func(http.ResponseWriter, *http.Request) { }(类型不匹配)。
正确写法只有一种核心形式:
-
func(next http.Handler) http.Handler—— 最通用,兼容任何实现ServeHTTP的类型 - 用
http.HandlerFunc包一层闭包是最轻量做法,比如:return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... }) - 别假设下游是
http.HandlerFunc;如果 handler 是自定义 struct 实现的http.Handler,中间件也得接收接口类型
拦截失败必须显式 return,不能只写响应
鉴权失败、参数校验不通过、IP 不在白名单——这些场景下,http.Error(w, "...", 401) 只是写响应头和 body,不会自动终止执行。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 漏掉
return,后续next.ServeHTTP()仍会调用,可能改数据库、发通知、泄露数据 - 所有提前退出路径都必须包含
return,且必须在next.ServeHTTP()之前 - 别在
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.ReadAll(r.Body) 一次,下游 handler 再读就是空字节。
- 调试时可这样缓存:
bodyBytes, _ := io.ReadAll(r.Body); r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - 生产环境慎用全量缓存,大文件或高并发易 OOM;建议只记录长度(
r.ContentLength),或采样前 1024 字节 - 别用
r.ParseForm()后再去读r.Body,它内部已经消费过一遍了 - 框架如 Gin 默认做了缓存,但原生
net/http没有,这点极易被忽略
Gin 中间件里用 c.Abort() 不等于终止当前函数
c.Abort() 只阻止后续中间件执行,不跳出当前函数体。若没 return,c.Next() 还会触发,导致重复响应或 panic。
- 典型错误:
c.JSON(403, ...); c.Abort(); c.Next();→c.Next()仍执行,可能触发multiple response.WriteHeader - 正确顺序:
c.JSON(403, ...); c.Abort(); return; -
c.Abort()不影响已执行的中间件(比如日志中间件已经打完日志了),只切断“尚未执行”的后续链 - 想彻底中断流程,
c.Abort()和return必须成对出现
最常被忽略的是:中间件顺序决定拦截范围,Recovery 要最外层,Auth 要在 Logging 之后但 Handler 之前;顺序错了,panic 可能没被捕获,或者日志里根本看不到未授权请求。

















