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 环境(如日志文件),输出不包含颜色代码。
工作原理
- 捕获 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)
}