Panic 恢复

Panic 会导致程序崩溃,在 Web 服务中必须妥善处理。Gin 的 Recovery 中间件可以捕获 panic,保证服务稳定运行。

内置 Recovery

gin.Default() 默认包含 Recovery:

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    
    r.GET("/panic", func(c *gin.Context) {
        panic("出错了!")
    })
    
    r.Run(":8080")
}

访问 /panic 时,服务不会崩溃,会返回 500 错误。

自定义 Recovery

func Recovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic recovered: %v\n%s", err, debug.Stack())
                
                c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                    "code":    500,
                    "message": "服务器内部错误",
                })
            }
        }()
        c.Next()
    }
}

r := gin.New()
r.Use(Recovery())

区分 Panic 类型

type PanicError struct {
    Code    int
    Message string
}

func (e *PanicError) Error() string {
    return e.Message
func TypedRecovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                var code int
                var message string
                
                switch e := err.(type) {
                case *PanicError:
                    code = e.Code
                    message = e.Message
                case error:
                    code = http.StatusInternalServerError
                    message = "服务器内部错误"
                    log.Printf("Panic: %v\n%s", e, debug.Stack())
                case string:
                    code = http.StatusInternalServerError
                    message = "服务器内部错误"
                    log.Printf("Panic: %s\n%s", e, debug.Stack())
                default:
                    code = http.StatusInternalServerError
                    message = "未知错误"
                    log.Printf("Panic: %v\n%s", err, debug.Stack())
                }
                
                c.AbortWithStatusJSON(code, gin.H{
                    "code":    code,
                    "message": message,
                })
            }
        }()
        c.Next()
    }
}

r.GET("/business-panic", func(c *gin.Context) {
    panic(&PanicError{Code: 400, Message: "业务错误"})
})

Panic 与错误通知

func NotifyRecovery(notify func(string)) gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                requestID, _ := c.Get("requestID")
                
                msg := fmt.Sprintf(
                    "[PANIC] RequestID: %v, Path: %s, Error: %v\n%s",
                    requestID,
                    c.Request.URL.Path,
                    err,
                    debug.Stack(),
                )
                
                log.Println(msg)
                notify(msg)
                
                c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                    "code":    500,
                    "message": "服务器内部错误",
                })
            }
        }()
        c.Next()
    }
}

func sendAlert(msg string) {
    fmt.Println("发送告警:", msg)
}

r.Use(NotifyRecovery(sendAlert))

主动触发 Panic

某些场景下可以主动 panic:

func MustGetUser(c *gin.Context) *User {
    user, exists := c.Get("currentUser")
    if !exists {
        panic(&PanicError{Code: 401, Message: "请先登录"})
    }
    return user.(*User)
}

r.GET("/profile", func(c *gin.Context) {
    user := MustGetUser(c)
    c.JSON(200, user)
})

Panic 与 Context

Panic 后 Context 仍然可用:

r.GET("/panic-context", func(c *gin.Context) {
    c.Set("before_panic", "panic 前设置的值")
    
    panic("测试 panic")
})

r.Use(func(c *gin.Context) {
    defer func() {
        if err := recover(); err != nil {
            val, _ := c.Get("before_panic")
            log.Printf("Panic: %v, Context value: %v", err, val)
            
            c.AbortWithStatusJSON(500, gin.H{"error": "内部错误"})
        }
    }()
    c.Next()
})

分环境处理

func EnvRecovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                requestID, _ := c.Get("requestID")
                
                if gin.Mode() == gin.DebugMode {
                    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                        "code":      500,
                        "message":   fmt.Sprintf("%v", err),
                        "requestId": requestID,
                        "stack":     string(debug.Stack()),
                    })
                } else {
                    log.Printf("[PANIC] %v\n%s", err, debug.Stack())
                    
                    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                        "code":      500,
                        "message":   "服务器内部错误",
                        "requestId": requestID,
                    })
                }
            }
        }()
        c.Next()
    }
}

与错误收集结合

func CombinedRecovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                c.Error(fmt.Errorf("panic: %v", err)).
                    SetType(gin.ErrorTypePrivate).
                    SetMeta(gin.H{"stack": string(debug.Stack())})
                
                c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                    "code":    500,
                    "message": "服务器内部错误",
                })
            }
        }()
        c.Next()
    }
}

小结

Recovery 中间件是生产环境必备的。自定义 Recovery 可以实现更精细的错误处理,比如区分错误类型、发送告警通知。开发环境可以返回详细错误信息,生产环境则隐藏敏感信息。记住,Panic 应该只用于真正的异常情况,不要滥用。