Skip to content
Go
Gin
中间件

中间件

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: after

c.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()
        }
    }
}