中间件
Gin 中的中间件是在路由处理函数**之前(以及可选地之后)**运行的函数。它们用于日志记录、认证、错误恢复和请求修改等横切关注点。
中间件附加级别
Gin 支持三个级别的中间件:
| 级别 | 注册方式 | 作用范围 | 示例 |
|---|---|---|---|
| 全局 | r.Use() |
所有路由 | Logger、Recovery |
| 分组 | group.Use() |
路由组内所有路由 | 认证中间件 |
| 路由级 | 作为参数传入路由方法 | 单个路由 | 特定限流、权限 |
// 全局
r.Use(gin.Logger())
// 分组
auth := r.Group("/admin")
auth.Use(AuthRequired())
// 路由级
r.GET("/secure", AuthRequired(), secureHandler)🚨 重要:中间件按注册顺序执行。理解这个执行顺序对自定义中间件的放置至关重要。
执行模型:洋葱模型
请求进来 → Middleware1(前) → Middleware2(前) → Handler → Middleware2(后) → Middleware1(后) → 响应func Middleware1() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("M1: before")
c.Next() // 传递给下一个
fmt.Println("M1: after")
}
}
func Middleware2() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("M2: before")
c.Next()
fmt.Println("M2: after")
}
}
// 输出:
// M1: before
// M2: before
// (handler runs)
// M2: after
// M1: afterc.Next() vs c.Abort()
| 方法 | 行为 | 使用场景 |
|---|---|---|
c.Next() |
将控制权传递给下一个处理函数,返回后继续执行 | 正常流程 |
c.Abort() |
阻止后续所有处理函数执行 | 认证失败、参数错误 |
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return // 后续处理函数不再执行
}
c.Next() // 认证通过,继续
}
}// c.Abort() 的常见变体
c.Abort() // 仅中止,不设置响应
c.AbortWithStatus(401) // 中止 + 状态码
c.AbortWithStatusJSON(401, gin.H{...}) // 中止 + JSON 响应
c.AbortWithError(500, err) // 中止 + 添加错误到 c.Errors自定义中间件
中间件签名:
type HandlerFunc func(*Context) 日志中间件示例
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// 设置下游可用的数据
c.Set("request_start", t)
c.Next() // 执行后续处理函数
// 下游处理完成后记录日志
latency := time.Since(t)
status := c.Writer.Status()
log.Printf("[%d] %s %s | %v", status, c.Request.Method, c.Request.URL.Path, latency)
}
}
func main() {
r := gin.New()
r.Use(Logger())
r.GET("/test", func(c *gin.Context) {
start := c.MustGet("request_start").(time.Time)
// ...
})
}自定义 Recovery
gin.CustomRecovery() 允许你自定义 panic 恢复行为(写入日志、发送告警、写入数据库等):
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
// recovered 是传给 panic() 的值
if err, ok := recovered.(string); ok {
// 记录到错误追踪系统
log.Printf("panic recovered: %s", err)
c.String(500, fmt.Sprintf("error: %s", err))
}
c.AbortWithStatus(500)
}))集中式错误处理中间件
在每个请求后检查 c.Errors 并返回统一错误格式:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next() // 先执行下游
// 检查是否收集了错误
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
// 根据错误类型决定状态码
status := 500
switch e := err.(type) {
case *ValidationError:
status = 422
case *NotFoundError:
status = 404
}
c.JSON(status, gin.H{
"success": false,
"message": err.Error(),
})
}
}
}
// 在处理函数中添加错误
r.GET("/user/:id", func(c *gin.Context) {
user, err := findUser(c.Param("id"))
if err != nil {
c.Error(err) // 添加到错误列表,不中断流程
return
}
c.JSON(200, user)
})💡 集中式错误处理避免了在每个处理函数中重复错误格式代码。它依赖
c.Error(err)将错误加入上下文,在中间件的c.Next()之后统一处理。
中间件中的 Goroutine
在中间件或处理函数中启动 goroutine 时,必须使用 c.Copy() 创建只读副本。
为什么需要 c.Copy()?
Gin 使用 sync.Pool 复用 gin.Context 对象。一旦处理函数返回,context 可能被分配给另一个请求。如果 goroutine 仍持有原始 context 引用,会导致:
- 竞态条件 — 两个 goroutine 同时读写同一个 context
- 数据损坏 — 读取到属于其他请求的数据
- Panic — 写入一个已被回收的 context
r.GET("/async", func(c *gin.Context) {
// ❌ 错误:在 goroutine 中直接使用 c
// go func() {
// log.Println(c.Request.URL.Path) // data race!
// }()
// ✅ 正确:先拷贝再使用
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)
log.Println("async done:", cCp.Request.URL.Path)
}()
c.JSON(200, gin.H{"status": "processing"})
})🚨 陷阱:
c.Copy()创建的是只读快照。不要在 goroutine 中通过副本修改响应——副本的Writer不会影响原始响应。
中间件中的数据传递
使用 c.Set() / c.Get() 在中间件和处理函数之间传递数据:
// 中间件中设置
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// ... 验证 token ...
c.Set("user_id", 12345)
c.Set("user_name", "willow")
c.Next()
}
}
// 处理函数中获取
r.GET("/profile", func(c *gin.Context) {
userID := c.MustGet("user_id").(int) // 断言失败时 panic
userName, exists := c.Get("user_name") // 安全获取
if !exists {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
})| 方法 | 行为 |
|---|---|
c.Set(key, value) |
设置值(覆盖已存在的) |
c.Get(key) |
获取值 + bool 存在性检查 |
c.MustGet(key) |
获取值,不存在则 panic |
c.GetString(key) |
获取字符串值 |
c.GetBool(key) |
获取布尔值 |
c.GetInt(key) |
获取整数 |
c.GetInt64(key) |
获取 int64 |
c.GetFloat64(key) |
获取 float64 |
c.GetTime(key) |
获取时间 |
c.GetDuration(key) |
获取时间段 |
c.GetStringSlice(key) |
获取字符串切片 |
c.GetStringMap(key) |
获取 string→any map |
c.GetStringMapString(key) |
获取 string→string map |
c.GetStringMapStringSlice(key) |
获取 string→[]string map |
常用内置中间件总结
| 中间件 | 来源 | 功能 |
|---|---|---|
gin.Logger() |
内置 | 请求日志输出 |
gin.Recovery() |
内置 | Panic 恢复 |
gin.LoggerWithConfig() |
内置 | 可配置 Logger |
gin.LoggerWithFormatter() |
内置 | 自定义日志格式 |
gin.CustomRecovery() |
内置 | 自定义恢复行为 |
gin.BasicAuth() |
内置 | HTTP Basic 认证 |
gin-contrib/cors |
社区 | CORS 跨域 |
gin-contrib/sessions |
社区 | 会话管理 |
gin-contrib/csrf |
社区 | CSRF 保护 |
gin-contrib/gzip |
社区 | Gzip 压缩 |
gin-contrib/cache |
社区 | 响应缓存 |
gin-contrib/secure |
社区 | 安全头 |
gin-contrib/requestid |
社区 | 请求 ID |
💡 优先使用社区维护的
gin-contrib/*中间件,而不是自己从头写。它们是经过测试和社区验证的。
上下文传递模式
向中间件传入参数
// 返回 gin.HandlerFunc 的函数可以接收外部参数
func RateLimit(maxRequests int) gin.HandlerFunc {
return func(c *gin.Context) {
// 使用 maxRequests ...
c.Next()
}
}
// 使用
r.Use(RateLimit(100))中间件执行条件化
// 仅在生产环境中应用
func ProductionOnly(handler gin.HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
if gin.Mode() == gin.ReleaseMode {
handler(c)
} else {
c.Next()
}
}
}