Slim

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

参考资料

切换 选择 esc 关闭搜索