recover必须在defer函数体内直接调用才有效,否则恒返回nil;它仅在panic正传播且同goroutine的defer执行时捕获panic,不能跨goroutine、不能间接调用、不能用于init或包级初始化。

recover必须在defer中调用才有效
Go里recover本身不捕获panic,它只是“取回”当前goroutine的panic值——前提是调用它时,该goroutine正处于panic后、运行defer栈的阶段。如果写在普通函数里,或者没被defer包裹,recover永远返回nil。
常见错误是把recover放在业务逻辑中间,比如:
func handle() {
recover() // 这里完全无效
doSomething()
}正确姿势只有一种:在defer函数体内,且该defer必须在panic发生前已注册。
- 必须用
defer func() { if r := recover(); r != nil { /* 处理 */ } }()形式,不能拆成两行 - 多个
defer时,recover只对最近一次未执行的panic生效;嵌套panic会被外层覆盖 - 主goroutine panic后进程退出,无法靠
recover续命;子goroutine可独立recover
上报前要区分panic类型和来源
不是所有panic都值得上报。空指针解引用、切片越界这类运行时panic,通常反映代码缺陷,需告警;而os.Exit触发的、或显式panic("health check failed")这类可控panic,可能只是流程控制手段。
立即学习“go语言免费学习笔记(深入)”;
建议在recover后做三层过滤:
- 检查
r是否为error接口,用errors.Is或类型断言识别已知业务panic(如errShutdown) - 用
runtime.Caller获取panic发生位置,排除testing包或第三方库内部的测试panic - 记录
runtime.Stack时限制长度(如4096字节),避免日志爆炸或OOM
示例关键片段:
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok && errors.Is(err, ErrSkipReport) {
return
}
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
log.Printf("PANIC: %v\n%s", r, buf[:n])
reportToSentry(r, buf[:n])
}
}()goroutine泄漏是recover最隐蔽的坑
recover能阻止panic终止goroutine,但不会自动清理资源。如果panic发生在持有锁、打开文件、启动子goroutine之后,recover后这些状态依然存在,极易引发死锁或句柄泄漏。
典型场景:
- 在
sync.Mutex.Lock()后panic,recover后忘记Unlock() - 用
http.Serve启HTTP服务时panic,recover后TCP连接未关闭,端口持续占用 - 启动了子goroutine监听channel,主goroutine recover后channel未关闭,子goroutine永久阻塞
解决思路不是“在recover里补全所有清理逻辑”,而是:把易panic的操作封装进带cleanup的闭包,或改用context统一控制生命周期。recover只负责兜底+上报,不负责修复。
不要在init或包级变量初始化中recover
Go规定init函数和包级变量初始化期间发生的panic无法被任何recover捕获,程序会直接崩溃。这是语言层面限制,不是写法问题。
现象是:程序启动几秒内闪退,日志里没有recover日志,go run报panic: xxx (exit status 2)。此时必须检查所有init函数、var = xxx()表达式里的调用链。
规避方式只有两个:
- 把可能panic的初始化逻辑延迟到
main函数或首次调用时执行 - 在
init里用try/catch风格预检(如if !isValidConfig() { os.Exit(1) }),避免真panic
这个限制常被忽略,直到线上服务因配置加载失败而无法启动才暴露。


















