Gin 路由中间件执行顺序怎么调

文章导读
Gin 的中间件执行顺序不看功能,只看注册顺序:谁先进入 handlers 链,谁就在 c.Next() 之前先执行,并在 c.Next() 之后最后返回。所以「顺序不对」通常属于三种情况之一:全局 Use 写在了路由注册之后;路由组嵌套时中间件挂在了不该挂的层级上;或者中间件内部把「校验并中止」写到了 c.Next() 后面。先把这三种情况区分开,再动代码。
📋 目录
  1. 一 先确认当前场景和目标
  2. 二 按这个顺序处理
  3. 三 改完后怎么验证
  4. 四 容易忽略的边界
A A

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 只对调用之后注册的路由生效,写在后面,那几条路由就拿不到中间件。这类问题不会报错,只会表现为「个别接口没有鉴权」,靠日志很难反推,值得先扫一遍源码顺序。

Gin 路由中间件执行顺序怎么调

第三步,组级中间件写进 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(),否则上下文会被复用,出现串数据。
  • 调整顺序会影响日志字段、耗时统计等已有逻辑。顺序不确定时,优先保留原顺序做局部拆分(给单条路由加中间件),而不是整体重排;改动先落在少量路由上观察,再推给其他路由。