Gin内置中间件详解

1.1 Recovery ------ panic 兜底

源码位置 :recovery.go

1.1.1 入口

Go 复制代码
// recovery.go:35-37
func Recovery() HandlerFunc {
    return RecoveryWithWriter(DefaultErrorWriter)
}

// recovery.go:40-42
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
    return RecoveryWithWriter(DefaultErrorWriter, handle)
}

// recovery.go:45-50
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
    if len(recovery) > 0 {
        return CustomRecoveryWithWriter(out, recovery[0])
    }
    return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}

// recovery.go:53-91
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
    var logger *log.Logger
    if out != nil {
        logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
    }
    return func(c *Context) {
        defer func() {
            if rec := recover(); rec != nil {
                // 1. 断开连接(让客户端知道服务端出错了)
                if logger != nil {
                    logger.Printf("%s\n%s", rec, stack(3))
                }
                c.Writer.WriteHeaderNow()
                // 2. 调用处理函数
                handle(c, rec)
            }
        }()
        c.Next()
    }
}

1.1.2 默认处理函数

Go 复制代码
// recovery.go:109-116
func defaultHandleRecovery(c *Context, err any) {
    e, ok := err.(error)
    if !ok {
        e = fmt.Errorf("%v", err)
    }
    c.Error(e)
    c.AbortWithStatus(http.StatusInternalServerError)
}

关键设计

  1. defer + recover():Go 语言 panic 兜底的标准模式
  1. panic 时机 :c.Next()defer 保护下执行,所以中间件链中任何位置的 panic 都会被捕获
  1. stack(3):打印堆栈(跳过 3 层调用:stack 自己、defer 函数、外层)
  1. c.Error(e):把错误收集到 Context,后续中间件(如 Logger)能看到
  1. c.AbortWithStatus(500):终止链并返回 500

1.1.3 stack() ------ 堆栈格式化

源码位置 :recovery.go:118-145

Go 复制代码
func stack(skip int) []byte {
    buf := new(bytes.Buffer)
    var (
        nLine    string
        lastFile string
        err      error
    )
    for i := skip; ; i++ {
        pc, file, line, ok := runtime.Caller(i)
        if !ok {
            break
        }
        fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
        if file != lastFile {
            nLine, err = readNthLine(file, line-1)   // 读源码那一行
            if err != nil { continue }
            lastFile = file
        }
        fmt.Fprintf(buf, "\t%s: %s\n", function(pc), cmp.Or(nLine, dunno))
    }
    return buf.Bytes()
}

亮点:

  • 通过 runtime.Caller 逐帧抓取
  • 缓存 lastFile 避免重复打开同一文件
  • readNthLine 读取源码对应行,让堆栈直接显示代码

1.1.4 自定义 Recovery

Go 复制代码
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered any) {
    slog.Error("panic", "err", recovered, "stack", debug.Stack())
    c.AbortWithStatusJSON(500, gin.H{"code": 9999, "msg": "internal error"})
}))

生产推荐:自定义 Recovery,返回结构化错误体。


1.2 Logger ------ 访问日志

源码位置 :logger.go

Go 复制代码
// logger.go:222-226
func Logger() HandlerFunc {
    return LoggerWithConfig(LoggerConfig{})
}

func LoggerWithFormatter(f LogFormatter) HandlerFunc {
    return LoggerWithConfig(LoggerConfig{Formatter: f})
}

func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
    return LoggerWithConfig(LoggerConfig{Output: out, SkipPaths: notlogged})
}

func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
    formatter := conf.Formatter
    if formatter == nil {
        formatter = defaultLogFormatter
    }
    out := conf.Output
    if out == nil { out = DefaultWriter }
    // ... 构建 skip map
    return func(c *Context) {
        start := time.Now()
        path := c.Request.URL.Path
        raw := c.Request.URL.RawQuery

        c.Next()                                       // ① 等请求处理完

        if _, ok := skip[path]; ok || /* Skip */ {
            return                                      // ② 跳过指定路径
        }

        param := LogFormatterParams{
            Request: c.Request,
            isTerm:  isTerm,
            Keys:    c.Keys,
        }
        param.TimeStamp = time.Now()
        param.Latency = param.TimeStamp.Sub(start)
        param.ClientIP = c.ClientIP()
        param.Method = c.Request.Method
        param.StatusCode = c.Writer.Status()
        param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
        param.BodySize = c.Writer.Size()
        if raw != "" { path = path + "?" + raw }
        param.Path = path

        fmt.Fprint(out, formatter(param))              // ③ 格式化输出
    }
}

1.2.1 入口

1.2.2 LogFormatterParams

源码位置 :logger.go:67-91

Go 复制代码
type LogFormatterParams struct {
    Request *http.Request
    TimeStamp time.Time
    StatusCode int
    Latency time.Duration
    ClientIP string
    Method string
    Path string
    ErrorMessage string
    isTerm bool
    BodySize int
    Keys map[any]any
}

并提供方法供 formatter 使用:

Go 复制代码
// logger.go:93-109
func (p *LogFormatterParams) StatusCodeColor() string {
    code := p.StatusCode
    switch {
    case code >= 200 && code < 300: return green
    case code >= 300 && code < 400: return white
    case code >= 400 && code < 500: return yellow
    default: return red
    }
}

终端会按状态码着色,这就是 Gin 默认日志漂亮的来源。

1.2.3 关键设计

  1. 记录在 c.Next()之后 :此时 c.Writer.Status() 才有值
  1. 颜色检测 :isatty 判断输出是否终端,终端才输出 ANSI 颜色码
  1. Skip 路径 :SkipPaths 静态跳过,Skip 函数动态跳过
  1. c.Keys透传 :让你能在中间件里 c.Set("user", ...) 后,日志里能打印

1.2.4 自定义日志格式

Go 复制代码
r.Use(gin.LoggerWithFormatter(func(p gin.LogFormatterParams) string {
    return fmt.Sprintf("%s - %s [%s] \"%s %s %d\" %d %s\n",
        p.ClientIP,
        p.Method,
        p.TimeStamp.Format(time.RFC3339),
        p.Method,
        p.Path,
        p.StatusCode,
        p.BodySize,
        p.Latency,
    )
}))

或者直接换更结构化的中间件(如 zap、slog),不用 gin.Logger

1.2.5 ErrorLogger

Go 复制代码
// logger.go:206-220
func ErrorLogger() HandlerFunc {
    return ErrorLoggerT(ErrorTypeAny)
}

func ErrorLoggerT(typ ErrorType) HandlerFunc {
    return func(c *Context) {
        c.Next()
        errors := c.Errors.ByType(typ)
        if len(errors) > 0 {
            c.JSON(-1, errors)             // ★ -1 表示不修改状态码
        }
    }
}

c.JSON(-1, ...)-1 来表示「保留现有状态码」,在 c.Status 里被特殊处理。


1.3 BasicAuth ------ HTTP 基础认证

源码位置 :auth.go

1.3.1 核心

Go 复制代码
// auth.go:44-68
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
    if realm == "" {
        realm = "Authorization Required"
    }
    realm = "Basic realm=" + strconv.Quote(realm)
    pairs := processAccounts(accounts)
    return func(c *Context) {
        user, found := pairs.searchCredential(c.requestHeader("Authorization"))
        if !found {
            c.Header("WWW-Authenticate", realm)
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }
        c.Set(AuthUserKey, user)        // 把当前用户名存到 Context
    }
}

func BasicAuth(accounts Accounts) HandlerFunc {
    return BasicAuthForRealm(accounts, "")
}

1.3.2 凭证预计算

Go 复制代码
// auth.go:76+
func processAccounts(accounts Accounts) authPairs {
    length := len(accounts)
    assert1(length > 0, "Empty list of authorized credentials")
    pairs := make(authPairs, 0, length)
    for user, password := range accounts {
        // 预计算 base64(user:password),避免每次请求都算
        value := authorizationHeader(user, password)
        pairs = append(pairs, authPair{value: value, user: user})
    }
    return pairs
}

1.3.3 常量时间比较(防时序攻击)

Go 复制代码
// auth.go:32-42
func (a authPairs) searchCredential(authValue string) (string, bool) {
    if authValue == "" {
        return "", false
    }
    for _, pair := range a {
        // ★ subtle.ConstantTimeCompare 防时序攻击
        if subtle.ConstantTimeCompare(
            bytesconv.StringToBytes(pair.value),
            bytesconv.StringToBytes(authValue),
        ) == 1 {
            return pair.user, true
        }
    }
    return "", false
}

💡设计意图 : subtle.ConstantTimeCompare而不是 ==

普通字符串比较在第一个不同字节就返回,攻击者可基于响应时间推测前缀。

常量时间比较无论结果都返回一样快。

1.3.4 使用

Go 复制代码
r := gin.Default()
api := r.Group("/api", gin.BasicAuth(gin.Accounts{
    "alice": "secret1",
    "bob":   "secret2",
}))
api.GET("/me", func(c *gin.Context) {
    user := c.MustGet(gin.AuthUserKey).(string)
    c.JSON(200, gin.H{"user": user})
})

1.4 三个中间件对比

|-------------|-----------------------------|-----------------|------------------|
| 维度 | Recovery | Logger | BasicAuth |
| 触发时机 | after(响应后) | after(响应后) | before(响应前) |
| 主要手段 | defer + recover | time.Since 计时 | Header 解析 + 常量比较 |
| 用到 c.Next | 是(必须,在 defer 内) | 是 | (短路) |
| 可能 Abort | 否(留给 defaultHandleRecovery) | 否 | 是(401) |


1.5 三段式中间件设计模式

从这三个例子可以提炼出Gin 中间件的三种典型模式:

模式 1:Before(短路型)

Go 复制代码
func MW() gin.HandlerFunc {
    return func(c *gin.Context) {
        if !check(c) {
            c.AbortWithStatus(401)
            return
        }
        c.Next()
    }
}

代表:BasicAuth、CORS、限流。

模式 2:After(统计/记录型)

Go 复制代码
func MW() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        log.Println(time.Since(start))
    }
}

代表:Logger、Prometheus 指标。

模式 3:Defer(兜底型)

Go 复制代码
func MW() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                handleErr(c, r)
            }
        }()
        c.Next()
    }
}

代表:Recovery。


1.6 性能优化点

源码中可以看到作者对性能的极致追求:

1.6.1 bytesconv.StringToBytes(零拷贝)

Go 复制代码
// 内部包 internal/bytesconv
func StringToBytes(s string) []byte {
    return unsafe.StringData(&s)  // 共享底层数组(简化示意)
}

避免 []byte(s) 触发的内存拷贝。在 Recovery、Logger 等热路径大量使用。

1.6.2 预计算 vs 即时计算

  • BasicAuth 预计算 base64(user:password)
  • Logger 预构建 skip map(而不是 slice,查找 O(1))

1.6.3 颜色检测一次性

Go 复制代码
isTerm := true  // 配置时检测一次,运行时直接读 bool
if w, ok := out.(*os.File); !ok || ... {
    isTerm = false
}

每条日志不再重新检测终端。


1.7 小结

  • ✅ Recovery:defer + recover 兜底 panic,配合 stack() 打印源码
  • ✅ Logger:在 c.Next() 后统计,通过 LogFormatterParams 自定义格式
  • ✅ BasicAuth:预计算凭证 + subtle.ConstantTimeCompare 防时序攻击
  • ✅ 三种中间件模式:Before / After / Defer
  • ✅ 内置中间件体现了 Gin 对性能的极致优化(bytesconv、预计算、一次性检测)
相关推荐
wtGEOyh6 小时前
2026,劲豆种业如何用生物育种“豆”振中国?
中间件
灯澜忆梦1 天前
【基于GO的Web开发15】gin路由和路由组
前端·后端·golang·gin
q567315231 天前
Curl 报 CONNECT tunnel failed, response 6xx:排查思路全解
数据库·网络协议·scrapy·http·中间件·http代理
灯澜忆梦1 天前
【基于GO的Web开发14】gin请求重定向
前端·后端·golang·gin
wtGEOyh2 天前
2026年劲豆种业:生物育种引领大豆产业新未来
中间件
远游客07132 天前
为什么用「年×100+月」做比较
算法·gin
cui_ruicheng2 天前
LangChain 应用开发(十二):Agent 中间件与内置中间件
人工智能·python·中间件·langchain
JackSparrow4142 天前
Kafka(七)集成Apache Avro+Apicurio Schema Registry以保障生产者与消费者的消息兼容性
java·中间件·rpc·kafka·apache·avro·schema-registry
灯澜忆梦2 天前
【基于GO的Web开发11】gin获取URL‑Path 路径参数
前端·后端·golang·html·gin