recover不能跨goroutine生效,因panic是goroutine局部的,主goroutine的defer+recover仅捕获自身panic;子goroutine需各自显式defer+recover,且recover仅止损不撤回副作用。

recover 不能跨 goroutine 生效,主 goroutine 的 defer + recover 捕获不到子 goroutine 的 panic —— 这是 Go 并发错误处理最常踩的坑。
为什么 main 里的 recover 抓不住 go func() 里的 panic
因为 panic 是 goroutine 局部的。主 goroutine 中注册的 defer func() { recover() }() 只监听自身调用栈;子 goroutine panic 后直接终止,不通知、不传播、不触发主 goroutine 的任何 defer。
常见错误现象:
- 程序没打印预期日志,却以 exit code 2 崩溃退出
- HTTP handler panic 后客户端断连,服务端无错误记录
- 使用
sync.WaitGroup等待子任务完成,但某个子 goroutine panic 导致资源泄漏(如未关闭的文件、连接)
根本原因:Go 运行时设计如此,不是 bug,是隔离性保障。想“全局兜底”?不存在。
每个可能 panic 的 goroutine 都要配 defer+recover
必须在每个 go func() 内部显式包裹 defer 和 recover,且顺序不能错:
-
defer必须写在可能 panic 的业务代码之前(否则 panic 发生时还没注册) - 不能写
defer recover()—— 语法错误,recover不是普通函数,只能在 defer 的函数体里调用 - 推荐用空参数匿名函数:
defer func() { if r := recover(); r != nil { /* 处理 */ } }(),避免被编译器内联优化掉 - recover 后不会继续执行 panic 后的语句,只负责“软着陆”,后续清理逻辑(如
close(ch)、resp.Body.Close())必须写在 defer 函数里
示例:
诊断并恢复通过 SSH 隧道连接的 OpenClaw 节点。用于解决配对必需错误、隧道冲突、远程端点错误以及 SSH 目标配置错误等问题。
go func(url string) {
defer func() {
if r := recover(); r != nil {
log.Printf("fetch %s panicked: %v", url, r)
// 这里做清理:比如 close(chan), http.CloseNotifier()
}
}()
resp, err := http.Get(url)
if err != nil {
log.Printf("fetch %s failed: %v", url, err)
return
}
defer resp.Body.Close() // 注意:这个 defer 在 recover 匿名函数内部才安全
// ... 处理响应
}(url)
用 goSafe 封装统一 recover 行为
重复写 defer+recover 很容易漏或写错。可封装一个通用函数,强制每个并发任务都受保护:
- 函数签名应为
func goSafe(f func()),接收纯函数,不暴露 goroutine 细节 - 内部必须用
go func() { defer ... f() }()结构,确保 recover 在同一 goroutine 内生效 - 建议加
debug.Stack()记录完整堆栈,方便定位 panic 来源(仅用于调试/告警,生产环境慎用全量 stack) - 不要把 recover 当 error 处理 —— 可预期错误(如网络超时、参数校验失败)应走
error返回,panic 仅用于真正不可恢复的编程错误(如空指针解引用、数组越界)
简版实现:
func goSafe(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered: %v\nstack: %s", r, debug.Stack())
}
}()
f()
}()
}
调用:goSafe(func() { panic("test") })
recover 后的状态一致性风险
recover 能让 goroutine 不崩溃,但无法回滚已执行的操作。这是最容易被忽略的复杂点:
- 如果 panic 前已向 channel 发送数据、已修改全局变量、已写入数据库,recover 不会撤销这些副作用
- 资源清理必须手动补全:比如 panic 前打开了文件,recover 后要显式
os.Remove或os.Truncate,否则留下脏数据 - 在 HTTP handler 或 RPC 方法中 recover,不能只打日志就完事——要确保返回明确的错误响应(如
http.Error(w, "internal error", 500)),否则客户端卡死
一句话:recover 是“止损”,不是“撤回”。状态是否一致,得靠你自己的逻辑兜底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










