登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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 等待结果,这里只用于暴露问题。

Go recover 同一 goroutine 作用域中主 goroutine、worker panic 与 deferred recover 的静态边界关系
图1:主 goroutine 与 worker goroutine 是两条独立控制栈,recover 只能落在发生 panic 的 worker 边界内。

把 recover 放进 worker 包装器

更稳妥的做法是规定:所有由业务代码启动的 worker,都从同一个包装器进入。recover 必须由这个包装器直接调用,不能再绕一层普通函数。

func safeGo(wg *sync.WaitGroup, errs chan

这里两个 defer 按后进先出执行:恢复逻辑先运行,随后 wg.Done() 标记 worker 结束。panic 被恢复后,safeGo 会正常返回,调用方收到的是一个普通 error。若 panic 值需要保留类型,可把 fmt.Errorf 换成项目自己的错误结构;不要把堆栈和敏感数据无条件写入日志。

Go safeGo 包装器将 worker panic 通过直接 deferred recover 转成 error channel 结果并由主流程验收
图2:safeGo 将 worker 的 panic 约束在自身 goroutine 内,再通过 error channel 和 WaitGroup 交给主流程验收。

用 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、结果结构或回调传递,主流程才能记录、重试或降级。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>