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)
}
关键设计
defer + recover():Go 语言 panic 兜底的标准模式
- panic 时机 :
c.Next()在defer保护下执行,所以中间件链中任何位置的 panic 都会被捕获
stack(3):打印堆栈(跳过 3 层调用:stack 自己、defer 函数、外层)
c.Error(e):把错误收集到 Context,后续中间件(如 Logger)能看到
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 关键设计
- 记录在
c.Next()之后 :此时c.Writer.Status()才有值
- 颜色检测 :
isatty判断输出是否终端,终端才输出 ANSI 颜色码
- Skip 路径 :
SkipPaths静态跳过,Skip函数动态跳过
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、预计算、一次性检测)