ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Gin内置中间件详解

Gin内置中间件详解 1.1Recovery—— panic 兜底源码位置:recovery.go1.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) }关键设计defer recover():Go 语言 panic 兜底的标准模式panic 时机:c.Next()在defer保护下执行,所以中间件链中任何位置的 panic 都会被捕获stack(3):打印堆栈(跳过 3 层调用:stack 自己、defer 函数、外层)c.Error(e):把错误收集到 Context,后续中间件(如 Logger)能看到c.AbortWithStatus(500):终止链并返回 5001.1.3stack()—— 堆栈格式化源码位置:recovery.go:118-145func 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 自定义 Recoveryr.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-91type 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 关键设计记录在c.Next()之后:此时c.Writer.Status()才有值颜色检测:isatty判断输出是否终端,终端才输出 ANSI 颜色码Skip 路径:SkipPaths静态跳过,Skip函数动态跳过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.go1.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 三个中间件对比维度RecoveryLoggerBasicAuth触发时机after(响应后)after(响应后)before(响应前)主要手段defer recovertime.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、预计算、一次性检测)
返回列表