Gin 的中间件执行顺序不看功能,只看注册顺序:谁先进入 handlers 链,谁就在 c.Next() 之前先执行,并在 c.Next() 之后最后返回。所以「顺序不对」通常属于三种情况之一:全局 Use 写在了路由注册之后;路由组嵌套时中间件挂在了不该挂的层级上;或者中间件内部把「校验并中止」写到了 c.Next() 后面。先把这三种情况区分开,再动代码。
Gin 的中间件顺序由注册顺序决定,不由名字或功能决定。先分清是全局 Use、路由组还是单条路由上的顺序问题,再按「兜底靠前、产值在前、早退在先」去调整;改完用一次请求打出 c.HandlerNames() 和请求日志时间线核对,能对上再全量铺开。
先确认当前场景和目标
先确认服务是用 gin.New() 还是 gin.Default() 起的:gin.Default() 已经带了 Logger 和 Recovery,自己再 Use 一次就会重复挂载。再确认三件事:全局中间件是否在所有路由注册之前;路由组嵌套了几层,中间件是挂在父组还是子组;哪些中间件依赖前一个中间件写入的上下文值,比如 token、用户 ID、trace id。
- 某条路由完全没有日志、没有鉴权:多半是
Use晚于路由注册,或这条路由挂在没带中间件的组上。 - 401 返回之后 handler 还在跑:中间件里没有
Abort,或Abort写在了c.Next()之后。 - 后置逻辑顺序看起来「反了」:这是
c.Next()之后的代码按注册逆序回溯,属于预期行为,不用改。
目标先定成四条:请求进入顺序可控;需要早退的地方能真正早退;panic 有兜底;上下文值的产出方永远排在使用方之前。这四条对上了,顺序问题基本就解决了。
按这个顺序处理
不要一次全改,改一层验一层。
第一步,先把全局骨架排好。Recovery 建议尽量靠前,这样后面所有中间件和 handler 的 panic 都能兜住。注意 gin.Default() 里是 Logger 在前、Recovery 在后,如果在意这层差异,直接用 gin.New() 显式排列会更清楚。
r := gin.New()
r.Use(gin.Recovery())
r.Use(gin.Logger())
r.Use(RequestID()) // 自己写的中间件按依赖顺序接在后面
api := r.Group("/api", AuthRequired())
api.GET("/orders", ListOrders)
第二步,把所有 r.Use(...) 移到路由注册之前。Gin 的 Use 只对调用之后注册的路由生效,写在后面,那几条路由就拿不到中间件。这类问题不会报错,只会表现为「个别接口没有鉴权」,靠日志很难反推,值得先扫一遍源码顺序。

第三步,组级中间件写进 Group 参数,从外到内。父组和子组的中间件会按「父组注册顺序 + 子组注册顺序」拼成一条链,子组不能反过来影响父组;如果发现某个组的中间件影响了别的组,通常是 Group 的调用位置写错了,而不是顺序本身有问题。
admin := r.Group("/admin", AuthRequired(), RequireRole("admin"))
admin.GET("/users", ListUsers)
第四步,单条路由要特殊处理时,直接写在 handler 前面。例如 r.GET("/health", RateLimit(), Health),比给整组加例外再在中间件里判断路径要干净。
第五步,中间件内部只保留一个 c.Next(),早退用 Abort 加 return。取值类逻辑放在 c.Next() 之前,统计耗时这类后置逻辑放在 c.Next() 之后。
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
return
}
c.Set("token", token)
c.Next()
}
}
改完后怎么验证
最直接的办法是在链首加一个只做打印的中间件,发一次真实请求,看链路顺序是否和预期一致。c.HandlerNames() 可以拿到当前路由的 handler 名称列表;较早的 Gin 版本可能没有这个方法,编译不过就改成自己打点,不要为了验证去升级依赖。
r.Use(func(c *gin.Context) {
fmt.Println("chain:", c.HandlerNames())
c.Next()
})
- 日志时间线:进入日志按注册正序出现,退出日志按逆序出现,成对且不缺失。
- 鉴权早退:
curl -i http://127.0.0.1:8080/api/orders不带 token 时应立即返回 401,且 handler 里的业务日志不出现;带上 token 时正常进入 handler。 - panic 兜底:在一个 handler 里故意 panic,确认客户端拿到 500、进程不退出,说明 Recovery 在链上且位置够靠外。
- 状态码一致:核对
c.Writer.Status()和实际响应码是否相同。如果日志记 200 而客户端拿到 401,说明有人在响应写出之后又改了状态码。
容易忽略的边界
Use对未匹配路由的行为要单独确认,需要的话用r.NoRoute(...)自己挂处理逻辑,不要默认全局中间件一定生效。Abort只能拦住还没执行的后续 handler;在c.Next()返回之后再调用,后续 handler 已经跑完了,拦不住。- 响应一旦写出,后面再改状态码不会生效;后置中间件不要试图纠正状态码或补写响应体。
- 在中间件里起 goroutine 并使用
c,需要c.Copy(),否则上下文会被复用,出现串数据。 - 调整顺序会影响日志字段、耗时统计等已有逻辑。顺序不确定时,优先保留原顺序做局部拆分(给单条路由加中间件),而不是整体重排;改动先落在少量路由上观察,再推给其他路由。