Recovery 中间件

Panic 是 Go 语言中不可忽视的问题,如果不处理,整个服务会崩溃。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 错误,日志会记录 panic 信息。

单独使用 Recovery

如果使用 gin.New(),需要手动添加:

func main() {
    r := gin.New()
    
    r.Use(gin.Recovery())
    
    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello")
    })
    
    r.Run(":8080")
}

自定义 Recovery 中间件

自定义 panic 处理逻辑:

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.Use(Recovery())

记录详细错误信息

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

r.Use(RequestID())
r.Use(DetailedRecovery())

区分错误类型

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

type CustomError struct {
    Code    int
    Message string
}

func (e *CustomError) Error() string {
    return e.Message
}

r.GET("/custom-error", func(c *gin.Context) {
    panic(&CustomError{Code: 400, Message: "自定义错误"})
})

集成错误通知

发生 panic 时发送通知:

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

func sendAlert(msg string) {
}

r.Use(NotifyRecovery(sendAlert))

与 Logger 配合

Recovery 和 Logger 配合使用:

func main() {
    r := gin.New()
    
    r.Use(gin.Logger())
    r.Use(gin.Recovery())
    
    r.GET("/test", func(c *gin.Context) {
        if c.Query("error") == "true" {
            panic("测试错误")
        }
        c.String(200, "OK")
    })
    
    r.Run(":8080")
}

生产环境建议

func ProductionRecovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                requestID := c.GetString("requestID")
                
                log.Printf(
                    "[RECOVERY] RequestID=%s, Path=%s, Error=%v",
                    requestID,
                    c.Request.URL.Path,
                    err,
                )
                
                if gin.Mode() == gin.DebugMode {
                    log.Printf("Stack:\n%s", debug.Stack())
                }
                
                c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                    "code":      500,
                    "message":   "Internal Server Error",
                    "requestId": requestID,
                })
            }
        }()
        c.Next()
    }
}

小结

Recovery 中间件是生产环境必备的,它能防止 panic 导致服务崩溃。建议配合日志记录和错误通知使用,方便排查问题。记得在开发环境打印完整的堆栈信息,生产环境可以精简。