中间件本质是func(*gin.context)函数,与路由handler类型相同,区别仅在于注册位置(use()或get());必须显式调用c.next()推进流程,否则请求卡住;前置逻辑在c.next()前执行(如token校验),后置逻辑在其后执行(如日志记录);c.abort()终止后续所有处理;执行遵循“洋葱模型”,即进层→到底→回弹。

中间件函数签名和执行时机怎么理解
中间件本质就是 func(*gin.Context),和路由 handler 类型完全一样。区别只在于你把它放在 Use() 里还是 GET() 里。
关键点是:中间件必须显式调用 c.Next() 才会让请求继续往下走;不调用就卡住,后续 handler 和中间件都不会执行。常见错误是写了逻辑但忘了 c.Next(),结果所有请求都静默失败,连 404 都不返回。
- 前置逻辑写在
c.Next()前,比如读 Header、校验 Token - 后置逻辑写在
c.Next()后,比如记录响应时间、写日志 -
c.Abort()会跳过后续所有中间件和 handler,适合权限拒绝、参数校验失败等场景 -
c.AbortWithStatusJSON(401, gin.H{"error": "xxx"})是终止 + 返回的组合操作,比先c.JSON()再c.Abort()更安全
为什么中间件要按“洋葱模型”执行
不是“从上到下顺序执行”,而是“进一层、再进一层、到底层 handler、再一层层弹回来”。比如注册了 m1、m2,再挂 handler,实际执行流是:m1 前 → m2 前 → handler → m2 后 → m1 后。
这个模型决定了:你在 m2 的后置代码里,能拿到 handler 已经设置的状态码和响应体大小,但不能拿到真实写出的内容——因为 Writer 还没 flush。
- 判断响应是否已发头,用
c.Writer.StatusWritten,不是c.Writer.Status() - 想在后置里修改响应体(比如加签名),得用
c.Writer.Write(),但要注意 Content-Length 可能已固定 - 如果 handler panic 了,
Recovery中间件靠 defer 捕获,它必须在最外层(即最早注册),否则捕不到
全局中间件和路由组中间件的优先级怎么算
注册顺序 ≠ 执行顺序。全局中间件(engine.Use())包裹整个路由树,而路由组中间件(group.Use())只作用于该 group 下的子路由,且**更靠近 handler**,所以执行时“插队”在全局中间件内层。
例如:engine.Use(m1),然后 api := engine.Group("/api"),再 api.Use(m2),最后 api.GET("/user", h)。执行时是:m1 前 → m2 前 → h → m2 后 → m1 后。
- 404 路由也会触发全局中间件,但不会触发任何路由组中间件(因为没匹配到 group)
- 想让某个中间件只对特定路径生效,别硬塞进全局,用 group 或直接挂到单个路由上更清晰
- 避免在全局中间件里做耗时操作(如 DB 查询),否则所有请求(包括健康检查)都受影响
c.Set() 和 c.Get() 安全传参的坑在哪
直接用字符串当 key(比如 c.Set("user_id", 123))看似方便,但极易冲突——多个中间件都用 "user_id",后设的会覆盖先设的。
正确做法是定义私有类型作 key:
type userIDKey struct{}
c.Set(userIDKey{}, 123)
v, ok := c.Get(userIDKey{})
这样 key 是类型唯一,不会被其他中间件误覆盖。另外注意:c.Set() 底层是 map 操作,高频路径(如 /health)反复调用会有微小开销,可考虑用 context.WithValue(c.Request.Context(), key, val) 替代。
真正容易被忽略的是:中间件之间传参不是为了“炫技”,而是为了把前置计算结果高效交给 handler。如果 handler 本身就能独立完成校验或构造数据,就别硬塞中间件传参——逻辑越简单,越不容易出错。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











