Gin中间件是按注册顺序入栈、逆序执行的函数链,必须显式调用c.Next()才能继续执行后续中间件和handler;c.Abort()可终止流程,全局中间件用r.Use(),路由组中间件需绑定到组实例,自定义中间件须返回gin.HandlerFunc类型。

中间件不是装饰器,是洋葱模型里的执行链
Gin 的中间件不是“加个注解就生效”的装饰器,它本质是一条按注册顺序入栈、按逆序执行的函数链。每个中间件必须显式调用 c.Next() 才会把控制权交给下一个环节;不调用就中断,后续中间件和路由 handler 都不会执行。
常见错误现象:写了中间件但日志没打印、鉴权没生效、请求卡住没响应——大概率是忘了写 c.Next(),或者在不该 abort 的地方调用了 c.Abort()。
- 前置逻辑(如解析 token、记录开始时间)写在
c.Next()之前 - 后置逻辑(如打日志、统计耗时、写响应头)写在
c.Next()之后 -
c.Abort()会跳过所有后续中间件和最终 handler,适合鉴权失败、参数校验不通过等场景 -
c.AbortWithStatusJSON()是更常用的终止方式,它同时设置状态码和响应体
全局中间件用 r.Use(),别和 gin.Default() 混用
gin.Default() 已经自动加载了 gin.Logger() 和 gin.Recovery(),如果你再手动 r.Use(gin.Logger()),就会重复打两遍日志;如果用 gin.New() 初始化引擎,则默认一个中间件都没有,全靠你手动加。
实际选型建议:
- 开发阶段用
gin.Default()快速启动,省心 - 生产环境建议用
gin.New()+ 显式r.Use(),避免隐式依赖,也方便替换或禁用默认日志(比如对接结构化日志系统时) - 想禁用默认 Recovery?直接用
gin.New(),它不带任何中间件
路由组中间件只对组内生效,r.Group("/api", mw1, mw2) 是常见误写
很多人写 r.Group("/api", mw1, mw2).GET(...),以为这是给该组加两个中间件——其实 r.Group() 的第二个及以后参数是变参,类型为 ...HandlerFunc,但它们**只作用于该 Group 的子路由,不作用于 Group 本身**。真正生效的是你往 Group 里注册的路由(比如 .GET、.POST),而不是 Group 这个“容器”。
正确写法:
-
api := r.Group("/api", mw1, mw2)→ 中间件已绑定到api实例 - 后续所有
api.GET(...)、api.POST(...)都自动携带这两个中间件 - 如果某个接口不需要某中间件,得单独拆出路由,或在中间件里做路径判断 +
c.Next()跳过
注意:中间件注册顺序 = 执行顺序,mw1 会比 mw2 更早进入“洋葱内层”,也就是更晚执行后置逻辑。
自定义中间件函数必须返回 gin.HandlerFunc
你不能直接写 func(c *gin.Context) { ... } 当作中间件传给 Use(),因为类型不匹配。r.Use() 接收的是 gin.HandlerFunc 类型,而匿名函数字面量需要显式转换或封装成函数值。
标准写法有两种:
- 独立函数:
func AuthMiddleware(c *gin.Context) { ... },然后r.Use(AuthMiddleware) - 闭包工厂:
func() gin.HandlerFunc { return func(c *gin.Context) { ... } },常用于带参数的中间件(比如限流阈值)
容易踩的坑:
- 闭包捕获变量时注意循环引用,尤其是 for 循环里创建多个中间件实例
- 中间件里用
c.Copy()再启 goroutine,否则并发访问*gin.Context可能 panic - 不要在中间件里修改
c.Request.URL或c.Request.Header后不调用c.Request.Header.Set()—— 原始 Header 是只读 map,直接赋值无效
c.Set()/c.Get())比看起来更微妙,尤其在 panic 恢复、异步 goroutine、重定向跳转等边界场景下,稍不注意就会丢失上下文或引发竞态。


















