gin中间件执行顺序由注册顺序决定,按handlerschain切片正向执行前置逻辑、逆向执行后置逻辑;c.next()必须显式调用以触发后续处理,否则链中断;全局、组级、路由级中间件按注册时机追加,不排序。

中间件注册顺序就是执行顺序
别被“嵌套”这个词带偏——Gin 里没有真正的函数嵌套调用,而是把所有中间件和 handler 按注册顺序拼成一个 HandlersChain 切片,然后按索引正向遍历执行前置逻辑,再逆向回溯执行后置逻辑。你写 r.Use(mw1)、r.Use(mw2)、r.GET("/x", mw3, handler),最终链是 [mw1, mw2, mw3, handler]。
常见错误现象:log 中间件没输出,但 auth 中间件提前 c.Abort() 或 panic;原因就是 auth 被注册在 log 后面,请求根本没走到 log 的 c.Next() 就中断了。
-
r.Use()添加的全局中间件,一定排在最前面 -
router.Group("/api").Use(auth)添加的组级中间件,插在全局之后、该组内路由 handler 之前 -
r.GET("/x", mw, handler)这种路由级中间件,只影响当前路由,且顺序紧挨着 handler - 所有中间件共享同一个
c.index,不能靠它跳过某层(比如if c.index == 2)
为什么 c.Next() 必须显式调用
c.Next() 不是“自动往下走”,而是手动触发链中下一个 handler 的执行点。漏掉它,后续所有中间件和 handler 都不会运行,请求就卡死在当前层。
典型错误写法:func(c *gin.Context) { fmt.Println("before"); c.Abort() } —— 这里没调 c.Next(),但也没问题,因为明确要终止;可如果本意是鉴权通过后继续,却忘了写 c.Next(),那后面日志、handler 全部静默失效。
- 前置逻辑写在
c.Next()前,后置逻辑写在c.Next()后 -
c.Abort()会阻止后续 handler 执行,但已进入的中间件仍会完成自己的后置逻辑(即c.Next()后代码) -
c.AbortWithStatusJSON()等终止方法同理,不跳过当前中间件的后置部分
全局 / 组级 / 路由级中间件的叠加规则
不是“优先级高低”,而是“注册时机决定插入位置”。Gin 不做排序,只做追加。
假设你有:r.Use(g1) → v1 := r.Group("/v1") → v1.Use(m1) → v1.GET("/a", h1) → r.GET("/b", m2, h2),那么:
- 访问
/v1/a的执行链是:g1 → m1 → h1 - 访问
/b的执行链是:g1 → m2 → h2 - 组本身不改变顺序,只是限定作用域;
v1.Use(m1)等价于把m1插入到所有v1子路由 handler 前
容易踩的坑:在 v1.Use(m1) 之后又调 r.Use(m2),m2 会出现在所有路由(包括 /v1/a)的最前面,而不是只影响后续注册的路由。
调试中间件执行顺序的最快方法
不用猜,直接打日志看 c.HandlerName() 和 c.FullPath(),或者用 fmt.Printf("[#%d] %s\n", c.Index(), c.HandlerName())。
更可靠的方式是临时加一个“计数中间件”:
func trace() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Printf("→ %s [%d] %s\n", c.Request.Method, c.Index(), c.FullPath())
c.Next()
fmt.Printf("← %s [%d] %s\n", c.Request.Method, c.Index(), c.FullPath())
}
}
把它放在最开头(r.Use(trace())),就能清晰看到每层进入和退出的顺序与索引。注意:c.Index() 是当前 handler 在链中的位置,不是执行次数。
真正容易被忽略的是:中间件的“后置逻辑”在 handler panic 后依然会执行(只要没被 c.Abort() 截断),而 Recovery 中间件之所以能捕获 panic,正是因为它在链末尾,且自身不 panic。











