中间件注册顺序即执行顺序,Gin中Use()调用顺序严格决定洋葱模型的进出顺序:先注册的先执行before、后执行after;全局中间件→路由组中间件→路由级中间件→handler。

中间件注册顺序决定执行顺序
注册顺序就是执行顺序,Use() 传入的多个中间件按参数位置从前到后排列,Group() 中声明的中间件也遵循同样规则。Gin 不会重排、不会合并、不会忽略——你写的顺序,就是它跑的顺序。
常见错误现象:AuthMiddleware 放在 LoggerMiddleware 后面,结果认证失败的请求没被记录;或者把 Recovery 放在某个耗时中间件之后,导致 panic 发生时已执行了部分前置逻辑,日志/监控状态错乱。
-
router.Use(m1, m2, m3)→ 执行顺序:m1 before → m2 before → m3 before → handler → m3 after → m2 after → m1 after - 路由级中间件(如
r.GET("/path", m1, m2, handler))只影响该路由,且优先级高于全局Use() - 路由组中间件(如
api := r.Group("/api", m1); api.GET("/x", handler))会插在全局中间件之后、路由 handler 之前
c.Next() 是洋葱模型的关键开关
中间件里不写 c.Next(),后续所有中间件和 handler 都不会执行;写了但放在中间或末尾之后再写逻辑,那些逻辑就属于“after”阶段。这是理解执行流的核心。
使用场景:权限校验中间件常在 c.Next() 前检查 token,校验失败就调用 c.Abort() 并返回,避免进入 handler;而日志中间件必须在 c.Next() 前后都写代码,才能统计完整耗时。
-
c.Next()不启动 goroutine,只是顺序调用 handlers slice 中下一个函数 - 一旦调用
c.Abort(),当前中间件及后续所有中间件的c.Next()都失效,但当前中间件c.Next()之后的代码仍会执行 - 不要在中间件里直接 return,除非你明确知道当前上下文已响应完毕(比如已调
c.JSON+c.Abort())
全局 Use() 和路由级中间件混合时的执行链
实际项目里往往三者共存:全局 Use()、路由组 Group(..., m)、路由方法上显式传入的中间件。它们最终被拼成一个 handlers slice,顺序固定为:全局 → 路由组 → 路由级 → handler。
性能影响:每个中间件都是一次函数调用,c.Next() 是循环跳转,开销极小;但若在中间件中做同步 I/O(如查 DB、调外部 API),会阻塞整个请求链路,应尽量避免。
- 示例链:
router.Use(g1, g2)+v1 := router.Group("/v1", r1)+v1.GET("/x", r2, h)→ 最终执行链:g1 → g2 → r1 → r2 → h - 注意:
gin.Default()内置了Logger()和Recovery(),等价于Use(Logger(), Recovery()) - 如果某个路由不需要某全局中间件(比如健康检查接口不该走鉴权),只能改用
gin.New()+ 手动注册,再对特定路由补全必要中间件
为什么日志中间件要放最外层
因为只有最外层中间件的 “before” 和 “after” 能包裹住整个请求生命周期。放在内层,比如在认证之后,就漏掉了认证失败的请求;放在 handler 之后,就统计不到 handler 内部 panic 导致的崩溃时间。
容易踩的坑:自定义日志中间件里只记录 c.Request.URL.Path 和状态码,却忘了加 c.Writer.Status() —— 这个值在 c.Next() 之后才确定,之前取出来永远是 0。
- 正确写法:before 记 start time,
c.Next(),after 取c.Writer.Status()和time.Since(start) - 别依赖
c.Writer.Size()判断是否已响应:有些中间件(如Recovery)可能已写入错误响应,但Size()仍为 0 - 调试时可在每个中间件前后打印
c.FullPath()和当前中间件名,快速定位执行卡点
c.Next() 的位置,决定了你是在拦截请求、还是在收尾响应。这两点一旦写错,问题往往藏得深、复现难。


















