Recovery 中间件
Recovery 中间件用于捕获应用程序中的 panic,防止服务器崩溃,并将错误优雅地传递给集中式错误处理器。
使用方法
基础用法
import ( "go-slim.dev/slim" "go-slim.dev/slim/middleware")func main() { s := slim.New() // Recovery 应该在 Logger 之后 s.Use(middleware.Logger()) s.Use(middleware.Recovery()) s.GET("/panic", func(c slim.Context) error { panic("something went wrong!") }) s.Start(":8080")}自定义配置
s := slim.New()s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{ StackSize: 4 << 10, // 4 KB DisableStackAll: false, DisablePrintStack: false,}))配置选项
RecoveryConfig
type RecoveryConfig struct { // StackSize 定义打印的堆栈大小 // 可选。默认值 4KB StackSize int // DisableStackAll 禁用格式化所有其他 goroutine 的堆栈跟踪 // 可选。默认值 false DisableStackAll bool // DisablePrintStack 禁用打印堆栈跟踪 // 可选。默认值 false DisablePrintStack bool}使用示例
1. 基本 Panic 恢复
s := slim.New()s.Use(middleware.Logger())s.Use(middleware.Recovery())s.GET("/panic", func(c slim.Context) error { panic("unexpected error!")})s.Start(":8080")当访问 /panic 时:
- Recovery 中间件捕获 panic
- 打印彩色堆栈跟踪到 stderr
- 返回 500 Internal Server Error
- 服务器继续运行
2. 禁用堆栈打印
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{ DisablePrintStack: true, // 不打印堆栈信息}))适用于生产环境,避免泄露敏感信息。
3. 只打印当前 Goroutine 的堆栈
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{ DisableStackAll: true, // 只打印当前 goroutine 的堆栈}))减少日志输出,提高可读性。
4. 自定义堆栈大小
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{ StackSize: 8 << 10, // 8 KB,用于更深的调用栈}))5. 与 Logger 中间件集成
s := slim.New()s.ErrorHandler = func(c slim.Context, err error) error { middleware.LogEnd(c, err) // 自定义错误响应 return c.JSON(500, map[string]string{ "error": "Internal Server Error", })}// Logger 应该在 Recovery 之前s.Use(middleware.Logger())s.Use(middleware.Recovery())s.GET("/panic", func(c slim.Context) error { panic("oops!")})输出示例:
2024/11/08 10:30:15.123 "GET /panic HTTP/1.1" from 127.0.0.1:54321 - 500 35B in 1.2ms panic: oops! -> github.com/your/project/handler.PanicHandler /path/to/handler.go:123 main.main /path/to/main.go:456. 处理特定类型的 Panic
s.Use(middleware.Recovery())s.Use(func(c slim.Context, next slim.HandlerFunc) error { defer func() { if r := recover(); r != nil { // 处理特定类型的 panic if err, ok := r.(error); ok { c.Error(err) return } // 重新抛出其他 panic panic(r) } }() return next(c)})堆栈跟踪格式
Recovery 中间件提供美化的堆栈跟踪输出:
彩色输出(TTY)
panic: something went wrong! -> go-slim.dev/slim/middleware.panicHandler /path/to/middleware.go:123 go-slim.dev/slim.(*Context).Next /path/to/context.go:456 main.main /path/to/main.go:789颜色说明:
- 红色箭头 (->):panic 发生的位置
- 紫色:包名
- 红色:函数名(panic 位置)
- 绿色:函数名(其他位置)
- 青色:文件名
- 绿色:行号
纯文本输出(非 TTY)
在非 TTY 环境(如日志文件),输出不包含颜色代码。
工作原理
- 捕获 Panic:使用
defer recover()捕获处理链中的 panic - 收集堆栈:调用
runtime.Stack()获取堆栈跟踪 - 格式化输出:美化堆栈信息并输出到日志
- 设置响应:
- 如果响应未写入,设置 500 状态码
- 如果是 WebSocket 升级,不修改响应
- 继续运行:服务器继续处理其他请求
中间件顺序
重要:Recovery 应该在 Logger 之后,这样可以确保 panic 被正确记录:
s.ErrorHandler = func(c slim.Context, err error) error { middleware.LogEnd(c, err) // 记录错误 return err}s.Use(middleware.Logger()) // Logger 在前s.Use(middleware.Recovery()) // Recovery 在后不会恢复的 Panic
Recovery 中间件不会恢复以下 panic:
http.ErrAbortHandler
s.GET("/abort", func(c slim.Context) error { panic(http.ErrAbortHandler) // 不会被恢复})http.ErrAbortHandler 是特殊的 panic,用于中止响应,Recovery 会重新抛出它。
最佳实践
1. 始终使用 Recovery 中间件
s := slim.New()// 在所有其他中间件之后(除了 Logger)s.Use(middleware.Logger())s.Use(middleware.Recovery())2. 生产环境配置
func newRecoveryMiddleware() slim.MiddlewareFunc { if os.Getenv("ENV") == "production" { return middleware.RecoveryWithConfig(middleware.RecoveryConfig{ DisablePrintStack: true, // 生产环境不打印堆栈 }) } return middleware.Recovery() // 开发环境打印完整堆栈}s.Use(newRecoveryMiddleware())3. 集中式错误处理
s.ErrorHandler = func(c slim.Context, err error) error { middleware.LogEnd(c, err) // 区分不同类型的错误 if he, ok := err.(*slim.HTTPError); ok { return c.JSON(he.Code, he) } // 隐藏内部错误细节 return c.JSON(500, map[string]string{ "error": "Internal Server Error", "id": c.Header(slim.HeaderXRequestID), })}4. 记录 Panic 到监控系统
type customLogEntry struct { logger *slog.Logger}func (c *customLogEntry) Panic(v any, stack []byte) { // 记录到日志系统 c.logger.Error("panic recovered", slog.Any("panic", v), slog.String("stack", string(stack))) // 发送到监控系统(如 Sentry) sentry.CaptureException(fmt.Errorf("panic: %v", v)) // 打印到 stderr middleware.PrintPrettyStack(v, stack)}s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ NewEntry: func(c slim.Context) middleware.LogEntry { return &customLogEntry{ logger: slog.Default(), } },}))s.Use(middleware.Recovery())5. 优雅降级
s.GET("/api/data", func(c slim.Context) error { defer func() { if r := recover(); r != nil { // 记录错误 log.Printf("Error processing request: %v", r) // 返回缓存数据或默认值 c.JSON(200, getCachedData()) } }() // 可能会 panic 的代码 data := fetchDataFromExternalAPI() return c.JSON(200, data)})与其他监控工具集成
与 Sentry 集成
import ( "github.com/getsentry/sentry-go" "go-slim.dev/slim/middleware")func init() { sentry.Init(sentry.ClientOptions{ Dsn: os.Getenv("SENTRY_DSN"), })}s.Use(func(c slim.Context, next slim.HandlerFunc) error { defer func() { if r := recover(); r != nil { // 发送到 Sentry sentry.CaptureException(fmt.Errorf("panic: %v", r)) // 重新抛出让 Recovery 中间件处理 panic(r) } }() return next(c)})s.Use(middleware.Recovery())自定义 Panic 处理器
func customRecovery() slim.MiddlewareFunc { return func(c slim.Context, next slim.HandlerFunc) error { defer func() { if r := recover(); r != nil { // 获取堆栈 stack := make([]byte, 4<<10) n := runtime.Stack(stack, false) stack = stack[:n] // 记录到日志 log.Printf("PANIC: %v\n%s", r, stack) // 发送告警 sendAlert(fmt.Sprintf("Panic: %v", r)) // 返回错误响应 if !c.Response().Written() { c.JSON(500, map[string]string{ "error": "Internal Server Error", }) } } }() return next(c) }}s.Use(customRecovery())测试 Recovery 中间件
func TestRecovery(t *testing.T) { s := slim.New() s.Use(middleware.Recovery()) s.GET("/panic", func(c slim.Context) error { panic("test panic") }) req := httptest.NewRequest("GET", "/panic", nil) rec := httptest.NewRecorder() s.ServeHTTP(rec, req) // 验证响应状态码 if rec.Code != 500 { t.Errorf("expected 500, got %d", rec.Code) }}默认配置
DefaultRecoveryConfig = RecoveryConfig{ StackSize: 4 << 10, // 4 KB DisableStackAll: false, DisablePrintStack: false,}性能考虑
- 堆栈收集:收集堆栈信息有一定开销,但只在 panic 时触发
- 堆栈大小:默认 4KB 足够大多数情况,如果调用栈很深可以增加
- 日志输出:美化输出需要解析堆栈,但不会显著影响性能
安全考虑
-
不要在生产环境暴露堆栈信息:
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{ DisablePrintStack: true, // 生产环境})) -
避免泄露敏感信息:
s.ErrorHandler = func(c slim.Context, err error) error { middleware.LogEnd(c, err) // 不要直接返回错误详情 return c.JSON(500, map[string]string{ "error": "Internal Server Error", })}
常见问题
Q: Recovery 能捕获异步代码中的 panic 吗?
A: 不能。Recovery 只能捕获当前 goroutine 中的 panic。异步代码需要单独处理:
s.GET("/async", func(c slim.Context) error { go func() { defer func() { if r := recover(); r != nil { log.Printf("Panic in goroutine: %v", r) } }() // 异步代码 panic("async panic") }() return c.String(200, "OK")})Q: 为什么 Recovery 要在 Logger 之后?
A: 这样可以确保 panic 被正确记录到日志中。如果 Recovery 在 Logger 之前,Logger 可能无法记录 panic 信息。
Q: 可以自定义 panic 响应吗?
A: 可以。Recovery 只是捕获 panic 并设置 500 状态码,实际响应内容由错误处理器决定:
s.ErrorHandler = func(c slim.Context, err error) error { return c.JSON(500, customErrorResponse)}