1.1 中间件 = HandlerFunc
源码位置 :gin.go:50-51
Go
type HandlerFunc func(*Context)
这就是 Gin 中间件的全部秘密------中间件和业务 handler 是同一种类型 。
中间件没有任何特殊待遇,它能:
- 在
c.Next()前做事(before)
- 在
c.Next()后做事(after)
- 调用
c.Abort()终止链
- 读写 Context 的任何字段
1.2 三级中间件的组装
Go
全局中间件 分组中间件 路由级中间件 业务 handler
(engine.Use) (group.Use) (r.GET(p, m1, m2, h))
│ │ │ │
└───────────────┴─────────────────┴──────────────────┘
↓
combineHandlers 合并成一个切片
↓
c.handlers (HandlersChain)
↓
c.Next() 顺序执行
1.2.1 全局中间件
源码位置 :gin.go:340-345
Go
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
注意第 2、3 行------Use 之后,404/405 的 handler 链也会重建,
让全局中间件对未匹配路由也生效。
1.2.2 分组中间件
Go
// routergroup.go:64-68
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
直接 append,简单粗暴。
1.2.3 路由级中间件
Go
r.GET("/admin", AuthMiddleware, AuditMiddleware, adminHandler)
AuthMiddleware 和 AuditMiddleware 作为可变参数传入,
和业务 handler 一起组成 handlers 切片。
1.2.4 combineHandlers 再回顾
源码位置 :routergroup.go:241-248
Go
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers) // 组中间件在前
copy(mergedHandlers[len(group.Handlers):], handlers) // 路由级在后
return mergedHandlers
}
结果 :[全局中间件..., 分组中间件..., 路由级中间件..., 业务 handler]
1.3 执行机制:洋葱模型剖析
1.3.1 一个标准中间件
Go
func MyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// ─── before ───
start := time.Now()
c.Next() // ← 把控制权交给下一个 handler
// ─── after ───
log.Printf("took %v", time.Since(start))
}
}
1.3.2 调用栈展开
假设注册:
Go
r.Use(A, B) // 全局
r.GET("/", C, D, handler) // 路由级
合并后 c.handlers = [A, B, C, D, handler],c.index = -1。
请求进入,Engine 调 c.Next():
Go
c.Next() index: -1 → 0
└─ A(c)
└─ c.Next() index: 0 → 1
└─ B(c)
└─ c.Next() index: 1 → 2
└─ C(c)
└─ c.Next() index: 2 → 3
└─ D(c)
└─ c.Next() index: 3 → 4
└─ handler(c)
(无 c.Next,直接 return)
(D 的 after 部分)
(C 的 after 部分)
(B 的 after 部分)
(A 的 after 部分)
执行顺序 :A.before → B.before → C.before → D.before → handler → D.after → C.after → B.after → A.after
这就是「洋葱模型」------从外到内进入,从内到外退出。
1.3.3 不调 Next() 会怎样?
Go
r.Use(func(c *gin.Context) {
// 没调 c.Next()
log.Println("A")
})
r.GET("/", func(c *gin.Context) {
log.Println("handler")
})
实际执行:A → handler(handler 仍会执行)。
原因 :Next() 内部就是个 for 循环。当 A 返回时,Engine 的入口 c.Next()调用还会继续推进循环(因为 index 仍小于 len(handlers))。
📌关键点 :你不调 c.Next(),handler 仍会被执行 。
调用 c.Next() 的意义只是「在它之后插入代码」,让那部分代码在 handler 之后跑。
1.4 abortIndex 的设计
源码位置 :context.go:55-57
Go
// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1 // = 63
为什么选 63?
math.MaxInt8 = 127,63 是它的一半
combineHandlers校验finalSize < 63,所以合法的 handler 链长度 < 63
Abort()把 index 设为 63,保证一定大于任何合法 index ,从而让Next()的循环终止
如果用 math.MaxInt8(127),会让 index++ 在 index=127 时溢出为 -128,导致死循环。选 63 留一半余量,避免溢出。
1.5 中间件的几种典型形态
1.5.1 Pass-through(只传递)
Go
func Header() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-Frame-Options", "DENY")
c.Next()
}
}
1.5.2 前置检查(可能终止)
Go
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
if !check(c) {
c.AbortWithStatusJSON(401, gin.H{"err": "unauthorized"})
return
}
c.Next()
}
}
1.5.3 After 逻辑(统计、记录)
Go
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
log.Printf("%s %s %d %v",
c.Request.Method, c.Request.URL.Path,
c.Writer.Status(), time.Since(start))
}
}
1.5.4 panic 兜底
Go
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
c.AbortWithStatus(500)
}
}()
c.Next()
}
}
💡 这就是 Gin 内置 Recovery 中间件的简化版,详见第 9 章。
1.5.5 修改请求或响应
Go
func RewritePath() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path == "/old" {
c.Request.URL.Path = "/new" // 改写,但不会重新匹配
}
c.Next()
}
}
⚠️注意 :改写 URL.Path不会重新走路由匹配 !
路由匹配已经完成,c.handlers 已经定了。要重新匹配,用 engine.HandleContext(c)(慎用)。
1.6 中间件的合并成本
每次注册路由,combineHandlers 都会新建一个切片。
这意味着中间件越多,每个路由占用的内存越大。
Go
// 假设有 5 个全局中间件 + 2 个分组中间件
r.Use(mw1, mw2, mw3, mw4, mw5)
api := r.Group("/api", mwA, mwB)
// 每注册一个路由,handlers 切片就有 8 个函数指针 + 1 个业务 handler
api.GET("/users", listUsers) // 9 个
api.GET("/posts", listPosts) // 又一个 9 个
// ... 1000 条路由 = 9000 个函数指针
实际上这不是问题,因为:
- 函数指针很小(8 字节)
- 这是注册时一次性成本,运行时不重复
combineHandlers用copy,内存连续,CPU cache 友好
但避免把 handler 创建放在循环里(每次都新建闭包):
Go
// ❌ 每次注册都创建新闭包
for _, p := range paths {
r.GET(p, func(c *gin.Context) { /* ... */ })
}
// ✅ 复用
h := func(c *gin.Context) { /* ... */ }
for _, p := range paths {
r.GET(p, h)
}
1.7 中间件 + 404/405 的特殊性
源码位置 :gin.go:340-345
Go
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers() // ★ 关键
engine.rebuild405Handlers()
return engine
}
Go
// gin.go:356-362
func (engine *Engine) rebuild404Handlers() {
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
}
func (engine *Engine) rebuild405Handlers() {
engine.allNoMethod = engine.combineHandlers(engine.noMethod)
}
作用 :每次 Use,404/405 handler 链都会重新合并,保证全局中间件对 404 也生效。
Go
r := gin.Default() // Handlers=[Logger, Recovery]
r.GET("/foo", fooH)
// 请求 /not-exist 会经过:
// 1. Logger (全局)
// 2. Recovery (全局)
// 3. noRoute handler (默认返回 "404 page not found")
这就是为什么 gin.Default() 模式下,404 请求也会被 Logger 记录。
1.8 中间件的常见反模式
1.8.1 在中间件里 goroutine 用 c
Go
// ❌ 错误
r.Use(func(c *gin.Context) {
go func() {
saveLog(c.ClientIP()) // c 可能已经被复用
}()
c.Next()
})
// ✅ 修复
r.Use(func(c *gin.Context) {
ip := c.ClientIP() // 提前取出值
go func() {
saveLog(ip)
}()
c.Next()
})
// ✅ 或复制
r.Use(func(c *gin.Context) {
cp := c.Copy()
go func() {
saveLog(cp.ClientIP()) // cp 是安全的副本
}()
c.Next()
})
1.8.2 Next() 不当回事
Go
// ❌ after 代码不会跑
r.Use(func(c *gin.Context) {
log.Println("before")
c.Next()
log.Println("after") // 这行会跑
})
// ❌ 漏写 c.Next(),但 handler 还是会跑
r.Use(func(c *gin.Context) {
log.Println("only before")
// 没 c.Next(),但 handler 仍执行(Engine 入口推动)
})
1.8.3 滥用 Abort
Go
// ❌ 用 Abort 跳过业务逻辑
r.Use(func(c *gin.Context) {
if randomCondition() {
c.AbortWithStatus(500)
}
// 业务 handler 被跳过,但你忘了告诉调用方
})
Abort 是「终止请求链」的工具,不是控制流跳转。要做条件分支,在 handler 内部写 if/else。
1.9 中间件执行顺序的设计哲学
为什么 Gin 选择**「全局 → 分组 → 路由级」**的顺序?
Go
越外层(全局) ────────────────────── 越内层(路由级)
通用关注点 具体业务
CORS、限流、日志 鉴权、参数预处理
执行早,责任重 执行晚,责任单一
实践经验:
Go
r.Use(
RequestID(), // 注入 ID,所有日志都用
Recovery(), // 最外层兜底 panic(在 Logger 之前,能记录 panic 日志)
Logger(), // 记录请求开始
CORS(), // CORS 在 Auth 前(预检不带 Auth)
RateLimit(), // 限流在 Auth 前(防爆破)
Auth(), // 鉴权
)
1.10 小结
- ✅ 中间件就是
HandlerFunc,没有任何特殊类型
- ✅ 全局 → 分组 → 路由级 通过
combineHandlers合并为单一 handlers 链
- ✅ 洋葱模型本质是同步函数调用栈,不是并发
- ✅
abortIndex = 63既防止越界也防止溢出
- ✅
Use后会重建 404/405 链,保证全局中间件对未匹配路由也生效
- ✅ goroutine 中用 Context 必须
c.Copy()或先取出值