跳到主要内容

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:45

6. 处理特定类型的 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 环境(如日志文件),输出不包含颜色代码。

工作原理

  1. 捕获 Panic:使用 defer recover() 捕获处理链中的 panic
  2. 收集堆栈:调用 runtime.Stack() 获取堆栈跟踪
  3. 格式化输出:美化堆栈信息并输出到日志
  4. 设置响应
    • 如果响应未写入,设置 500 状态码
    • 如果是 WebSocket 升级,不修改响应
  5. 继续运行:服务器继续处理其他请求

中间件顺序

重要: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,
}

性能考虑

  1. 堆栈收集:收集堆栈信息有一定开销,但只在 panic 时触发
  2. 堆栈大小:默认 4KB 足够大多数情况,如果调用栈很深可以增加
  3. 日志输出:美化输出需要解析堆栈,但不会显著影响性能

安全考虑

  1. 不要在生产环境暴露堆栈信息

    s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
    DisablePrintStack: true, // 生产环境
    }))
  2. 避免泄露敏感信息

    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)
}

参考资料