recover必须在defer函数中调用才有效,否则返回nil;它仅在panic传播且当前defer执行时生效,需为每个goroutine单独注册defer,且recover后应清理资源或显式退出。

recover必须在defer函数里调用,否则永远返回nil
这是最常踩的坑:把recover()写在普通代码流中,比如函数开头、if分支里,甚至main()顶层,它一定返回nil。因为recover()只在panic正在传播、且当前defer正在执行时才有效。
正确结构只有一种:
defer func() {
if r := recover(); r != nil {
// 处理 panic 值
}
}()
- 这个
defer语句必须出现在可能触发panic的代码之前——哪怕只差一行,晚了就失效 - 不能写成
if r := recover(); r != nil { }单独一行 - 不能在goroutine外层函数里注册
defer去捕获子goroutine的panic(每个goroutine都得单独加)
封装成工具函数时,必须确保defer在goroutine入口处注册
想统一处理所有goroutine的panic,不能靠“全局注册一次”,而要让每个goroutine自己带defer。常见做法是封装一个goSafe函数:
func goSafe(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in goroutine: %v", r)
// 可选:上报、告警、记录堆栈
}
}()
f()
}()
}
- 调用方式:
goSafe(func() { panic("oops") }) - 注意:不能传入已带
defer+recover的函数,否则会重复捕获或漏捕获 - HTTP handler里起子goroutine时,也得用这个包装,否则外面的中间件
defer完全收不到 - 如果用
errgroup.Group,它的Go()方法内部已做了类似封装,可直接信任
recover后不能复用原状态,必须显式清理或退出
recover()只是终止栈回溯、让函数继续执行,不代表程序状态安全。越界访问slice后recover,那个slice可能已处于未定义行为;向已close的channel再写入,大概率二次panic。
立即学习“go语言免费学习笔记(深入)”;
- recover后应立即关闭文件、释放锁、取消context等资源
- 避免在recover块里继续调用依赖原变量的函数(比如对刚panic过的map做range)
- 推荐显式
return或panic新错误,而不是“硬撑”执行后续逻辑 - 如果必须继续,先做状态校验:检查
map是否为nil、channel是否closed、指针是否有效
统一异常处理中间件里,recover值要转成error再透出
HTTP中间件中捕获到r := recover(),它是个interface{},不是error。直接当error用会导致类型断言失败,可能二次panic。
- 记录日志时用
fmt.Sprint(r)或fmt.Sprintf("%v", r),别用r.(error).Error() - 若需统一转为
error,建议封装:errors.New(fmt.Sprint(r)) - 不要试图在中间件里“恢复并重试”,recover后该请求上下文已不可信,应直接返回500并记录原始panic值
- 若要区分业务panic和运行时panic,可在panic时传入自定义error类型,recover后做类型断言判断
defer+recover只对当前goroutine生效,哪怕只漏掉一个go func()没包进goSafe,它崩溃时就会静默退出、连接卡死、事务没回滚。


















