自定义中间件必须返回gin.handlerfunc类型且显式调用c.next()或c.abort(),因gin的use()和路由方法仅接受该类型(即func(*gin.context)),c.next()是链式执行核心开关,漏调则请求卡住,未返回正确类型会导致编译错误或panic。

自定义中间件必须返回 gin.HandlerFunc 类型,且内部必须显式调用 c.Next() 或 c.Abort(),否则请求会卡住或跳过后续逻辑。
中间件函数签名和返回值为什么必须是 gin.HandlerFunc
因为 Gin 的 Use() 和路由注册方法(如 GET())只接受 gin.HandlerFunc 类型参数。这个类型本质就是 func(*gin.Context),它不是普通函数,而是能被 Gin 调度执行的“处理器单元”。
常见错误写法:func(c *gin.Context) { ... } 直接作为参数传入 —— 这在语法上合法,但容易漏掉闭包返回逻辑,导致无法复用;正确结构必须是函数工厂模式:
- 外层函数负责接收配置(如日志前缀、超时阈值),返回一个中间件函数
- 内层函数才是真正的
gin.HandlerFunc,接收*gin.Context并处理 - 不返回
gin.HandlerFunc的函数不能被Use()接收,编译会报错
c.Next() 不是可选的,它是流程控制的核心开关
调用 c.Next() 才会让请求继续往下走:进入下一个中间件,或最终落到路由处理函数。没调用它,整个链就停在那里,后续所有逻辑(包括业务 handler)都不会执行。
典型误用场景:
- 鉴权失败后只写响应但忘记调用
c.Abort()→ 请求仍会继续执行业务逻辑,造成越权 - 耗时统计中间件里漏掉
c.Next()→ 日志只打“开始”,永远等不到“结束” - 在
c.Next()后还试图修改响应体(如c.JSON()),但前面 handler 已写过 header → 触发http: multiple response.WriteHeader callspanic
全局、路由组、单路由三种注册方式的区别与风险点
注册位置决定作用域和执行顺序,稍不注意就会覆盖或遗漏:
-
router.Use(m1, m2):全局生效,所有路由(包括未匹配的 404)都会经过,适合Logger、Recovery -
group := router.Group("/api"); group.Use(authMw):仅该分组下路由生效,适合按模块隔离的鉴权或版本控制 -
router.GET("/admin", authMw, adminHandler):仅此接口生效,注意中间件要写在 handler 前面,顺序反了就无效
关键细节:Use() 是追加,但路由定义时传入的中间件是“覆盖”当前作用域默认链 —— 比如 router.Use(m1) 后又在某个 GET() 里传 m2,那这个接口实际执行的是 m2 → handler,m1 不会自动叠加。
中间件里读写上下文数据要用 c.Set() 和 c.Get(),别直接改结构体字段
*gin.Context 是每次请求新建的实例,但它内部维护了一个 map[string]interface{} 存储键值对。跨中间件传递数据(比如解析出的用户 ID、请求 ID)必须走这个机制。
错误做法:c.UserID = 123(*gin.Context 没有这个字段,编译不过)或自己声明全局变量(并发不安全)。
正确做法:
- 前置中间件中:
c.Set("user_id", userID) - 后续中间件或 handler 中:
if uid, ok := c.Get("user_id"); ok { ... } - 避免 key 冲突:建议统一前缀,如
"auth.user_id"、"trace.request_id"
这个 map 是线程安全的,但值本身不自动深拷贝 —— 如果存的是指针或 map,多个中间件修改同一对象仍可能引发竞态,这点容易被忽略。











