
panic 不能用 defer + recover 捕获的常见场景
Go 的 recover 只能在 defer 函数中、且 panic 正在传播时生效。一旦 panic 发生在 goroutine 外部未被包裹,或发生在 runtime 级别崩溃(如栈溢出、内存损坏),recover 就完全失效。
- 主 goroutine 中没包
defer+recover的顶层函数 panic → 进程直接退出,无捕获机会 - 新起的 goroutine(比如
go http.HandleFunc(...))里 panic → 不会触发主 goroutine 的recover,必须在该 goroutine 内部单独处理 -
fatal error: stack overflow或fatal error: all goroutines are asleep - deadlock→ 这些是 runtime 终止信号,recover根本不触发
http.Server 的 panic 自动恢复要手动开启
默认情况下,http.Server 遇到 handler panic 会打印堆栈并关闭连接,但不会 recover,更不会返回 500。想让服务持续可用且可控返回,得显式启用 http.Server.ErrorLog 并配合中间件或包装 handler。
- 标准库不提供开箱即用的 panic 恢复;
http.DefaultServeMux完全不处理 panic - 推荐做法:用 wrapper 函数包住每个 handler,内部加
defer+recover,统一记录日志并返回http.StatusInternalServerError - 示例:
func recoverPanic(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Printf("PANIC in %s %s: %+v", r.Method, r.URL.Path, err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() next(w, r) } }
告警集成的关键不是“捕获”,而是“区分可恢复性”
把所有 panic 都发告警,等于没有告警。真正要拦截的是那些重复发生、影响核心路径、或伴随特定错误模式(如 DB 连接失败后连续 panic)的问题。
- 不要对每次 panic 都触发企业微信/钉钉推送;先写入结构化日志(含 traceID、panic msg、goroutine stack),再由日志系统按规则过滤告警
- 避免在
recover里直接调用远程告警接口(如 HTTP POST),网络超时或失败会导致 handler 卡住;应写本地 channel 或异步队列,交由独立 goroutine 发送 - 注意 panic 值类型:如果
panic("user not found")是业务逻辑误用,不该告警;但panic(interface conversion: interface {} is nil, not *model.User)往往暴露空指针隐患,需标记为高优
Test 中故意 panic 后 recover 的陷阱
单元测试里写 recover 验证 panic 行为,容易因 goroutine 生命周期或 test helper 设计不当导致误判。
立即学习“go语言免费学习笔记(深入)”;
- 在
test函数里直接panic然后recover—— 可行,但必须确保recover在同一 goroutine、同一 defer 链中 - 若被测函数启动了新 goroutine 并在其中 panic,test 主 goroutine 的
recover捕不到;要用sync.WaitGroup+chan等待结果 - 别依赖
testing.T.Fatal替代 recover 判断;它只是终止当前 test,不代表 panic 被程序逻辑捕获
实际线上最常漏掉的是:HTTP handler 外的全局初始化 panic(比如 init 函数里加载配置失败)、或第三方库回调中抛出的 panic(如 prometheus 的 promhttp.Handler() 内部)。这些地方没有自然的 defer 上下文,得靠 wrapper 包一层,或者用 runtime.SetPanicHandler(Go 1.22+)做兜底 —— 但后者仅适用于极简日志记录,无法做 recover 操作。










