☰
Gin内置中间件详解
2026/11/6 14:37:15 网站建设 项目流程

1.1Recovery—— panic 兜底

源码位置:recovery.go

1.1.1 入口

// 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 默认处理函数

// 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.3stack()—— 堆栈格式化

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

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

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.2Logger—— 访问日志

源码位置:logger.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.2LogFormatterParams

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

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 使用:

// 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 自定义日志格式

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.5ErrorLogger

// 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.3BasicAuth—— HTTP 基础认证

源码位置:auth.go

1.3.1 核心

// 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 凭证预计算

// 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 常量时间比较(防时序攻击)

// 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 使用

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(短路型)

func MW() gin.HandlerFunc { return func(c *gin.Context) { if !check(c) { c.AbortWithStatus(401) return } c.Next() } }

代表:BasicAuth、CORS、限流。

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

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

代表:Logger、Prometheus 指标。

模式 3:Defer(兜底型)

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.1bytesconv.StringToBytes(零拷贝)

// 内部包 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 颜色检测一次性

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、预计算、一次性检测)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询