c.next() 是洋葱模型的执行开关,控制中间件“进入”和“返回”两个阶段的执行时机,而非简单跳转;其前后代码分别在请求下行和响应上行时运行,决定日志、耗时、数据读取等逻辑的正确位置。

c.Next() 是洋葱模型的执行开关,不是“继续往下走”的直觉操作
很多人误以为 c.Next() 就是简单跳到下一个中间件,结果发现日志没打、耗时统计不准、甚至请求直接 404。根本原因在于:它不控制“是否执行”,只控制“执行时机”——c.Next() 前后的代码,分别在“进入”和“返回”两个阶段运行。
比如注册顺序是 Logger → Auth → Handler,实际执行流是:
- Logger(前半段)→ Auth(前半段)→ Handler → Auth(后半段)→ Logger(后半段)
这决定了:
- 想在请求开始前记录时间,必须写在
c.Next()前 - 想读取 handler 设置的
c.Set("user", u),必须写在c.Next()后 - 如果某个中间件没调
c.Next(),后续所有中间件和 handler 都不会执行
中间件注册顺序 = 进入顺序,但不等于完整执行顺序
注册顺序决定“洋葱层从外到内的包裹关系”,但每层的后半段是逆序执行的。这点在调试时特别容易踩坑:比如你加了三个中间件 A、B、C,并在每个里都打印 "A enter" / "A exit",看到的输出会是:
A enter B enter C enter C exit B exit A exit
常见错误场景:
- 在
c.Next()后读取c.Request.Body—— 此时 body 已被 handler 读过,返回空 - 用
c.Abort()后还写了c.JSON()—— 可能触发 panic:“header already written” - 多个中间件都调
c.Set()同一个 key,后注册的会覆盖先注册的(因为c.Set是写入 context,不是追加)
全局 Use、分组 Use 和路由级中间件的优先级差异
中间件不是“谁先注册谁先跑完”,而是按“作用域嵌套深度”叠加。Gin 把所有中间件和 handler 合并成一个 HandlersChain 切片,合并规则是:分组中间件 + 路由中间件 + handler。
例如:
r := gin.Default()
r.Use(mwGlobal)
api := r.Group("/api")
api.Use(mwAPI)
api.GET("/user", mwRoute, handler)
最终该路由的执行链是:[mwGlobal, mwAPI, mwRoute, handler]。注意:
-
r.Use()注册的中间件对所有子路由生效,包括未显式挂载的 -
group.Use()只影响该 group 下新增的路由,不影响已注册的路由 - 路由级中间件(如
GET(path, mw, h))只对该路由生效,且排在 group 中间件之后 - 如果某个中间件调用了
c.Abort(),它后面的全部项(包括其他中间件和 handler)都不会执行
为什么 c.Next() 不报错却没效果?检查这三件事
最常遇到的现象:中间件函数写了,也注册了,c.Next() 也调了,但 handler 就是不执行。通常卡在这几个点:
- 中间件函数签名不是
gin.HandlerFunc类型,比如漏了*gin.Context参数或返回类型不对 - 中间件被注册到了错误的 group 或 router 实例上(比如你在
apigroup 上Use,却往r上挂路由) - handler 函数本身 panic 了,但没 recover,导致
c.Next()后的代码没机会执行(此时看日志会发现只有“enter”没有“exit”)
调试建议:在中间件开头加 log.Printf("[MW] %s start", runtime.FuncForPC(reflect.ValueOf(c.Handler).Pointer()).Name()),确认它是否真被调用;再在 c.Next() 前后各打一行 log,观察是否成对出现。
真正难处理的,从来不是怎么写 c.Next(),而是怎么判断它该不该被调、在哪儿被调、以及调完之后 context 里还剩什么。











