Go recover 为什么捕获不到另一个协程的 panic
来源:17golang原创
时间:2026-09-05 21:58:48 238浏览 收藏
很多 Go 程序第一次给并发任务加兜底时,会把 defer recover() 写在 main 或父函数里,然后发现 worker 里的 panic 仍然让程序退出。原因不是 recover 失效,而是它有严格的 goroutine 作用域:go worker() 建立了独立的控制栈,父 goroutine 的 defer 不会跨过去。要接住这个 panic,必须在 worker 入口安装一个直接调用 recover 的 deferred function。
记住一句话:panic 在哪条 goroutine 栈上发生,recover 就要在同一条栈的 deferred function 里调用;跨 goroutine 只能通过 channel、返回值或其他协作方式传递处理结果。
- 父函数的 recover 只能处理当前 goroutine 栈上的 panic。
- worker 应由包装器统一 defer recover,并把 panic 值转换为 error。
- 用 error channel 汇总异常,用 WaitGroup 负责生命周期,避免主流程提前退出或关闭通道过早。
先用一个最小例子看清作用域
下面的代码故意把 recover 放在父函数中。它只能保护 main 自己调用的同步代码,保护不了新 goroutine:
func main() {
defer func() {
fmt.Println("parent recover:", recover())
}()
go func() {
panic("worker failed")
}()
time.Sleep(100 * time.Millisecond)
}
即使父函数还没有返回,worker 也已经在自己的控制栈上 panic。规范描述的恢复条件是“同一 goroutine 中发生 panic”,所以父函数的 deferred function 没有机会接管它。真实服务里不要用 sleep 等待结果,这里只用于暴露问题。

把 recover 放进 worker 包装器
更稳妥的做法是规定:所有由业务代码启动的 worker,都从同一个包装器进入。recover 必须由这个包装器直接调用,不能再绕一层普通函数。
func safeGo(wg *sync.WaitGroup, errs chan
这里两个 defer 按后进先出执行:恢复逻辑先运行,随后 wg.Done() 标记 worker 结束。panic 被恢复后,safeGo 会正常返回,调用方收到的是一个普通 error。若 panic 值需要保留类型,可把 fmt.Errorf 换成项目自己的错误结构;不要把堆栈和敏感数据无条件写入日志。

用 channel 和 WaitGroup 汇总一个可验收的小项目
把包装器接到主流程时,关键是先登记 worker,再等待全部 worker 结束,最后关闭结果 channel:
func main() {
var wg sync.WaitGroup
errs := make(chan error, 2)
jobs := []func(){
func() { fmt.Println("job A ok") },
func() { panic("job B failed") },
}
wg.Add(len(jobs))
for _, job := range jobs {
go safeGo(&wg, errs, job)
}
go func() {
wg.Wait()
close(errs)
}()
for err := range errs {
fmt.Println("received:", err)
}
fmt.Println("all workers checked")
}
errs 设为有缓冲 channel,是为了让最后一个 worker 在收集循环尚未调度时也能交付错误;缓冲数量应按并发策略设计,不能当成无限队列。只有等待所有发送者结束后才能 close(errs),否则会出现 “send on closed channel”。验收标准也很具体:能看到 worker panic: job B failed,随后输出 all workers checked,进程不会因该业务 panic 直接终止。
三个容易让 recover 再次失效的边界
| 现象 | 原因 | 处理 |
|---|---|---|
| 父函数 recover 没反应 | panic 在另一个 goroutine | 把 defer recover 放到 worker 包装器 |
| recover 返回 nil | 不在 panic 状态,或不是 deferred function 直接调用 | 检查调用位置,避免在 helper 中间接调用 |
| 程序不退出但任务结果丢失 | 只恢复了 panic,没有传递异常 | 转为 error,通过 channel 或结果对象汇总 |
还要区分“可恢复的业务边界”和“程序不变量被破坏”。对单个任务输入异常,可以恢复并返回 error;共享状态损坏、严重运行时错误或无法保证继续工作的情况,不应仅靠 recover 掩盖。Go 1.21 起,直接调用 panic(nil) 也会触发非 nil 的运行时 panic,代码仍应把 recover 当作最后一道边界,而不是日常错误返回的替代品。
相关问题
recover 能不能在另一个函数里调用?
可以调用,但只有它本身就是直接被 defer 的函数时,才具备恢复语义。若 defer 的函数先调用 helper,再由 helper 调 recover,通常会得到 nil。
每个 goroutine 都要写一遍 recover 吗?
不必复制代码,把启动入口统一收口到 safeGo 之类的包装器即可。无法控制的第三方 goroutine 不能由你的父函数代为恢复。
recover 后还需要返回 error 吗?
需要。recover 只改变 panic 的控制流,不会自动告诉业务方任务失败;通过 error channel、结果结构或回调传递,主流程才能记录、重试或降级。
-
263 收藏
-
Golang · Go问答 | 31分钟前 | time · go · 超时配置 · time.Duration · time.Duration Go超时 time.Second time.ParseDuration354 收藏
-
382 收藏
-
317 收藏
-
Golang · Go问答 | 1小时前 | 结构体 · Go问答 · encoding/json · JSON序列化 · Go json.Marshal omitempty 结构体转JSON 空对象 导出字段374 收藏
-
350 收藏
-
468 收藏
-
130 收藏
-
213 收藏
-
358 收藏
-
101 收藏
-
386 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习