跳到主要内容Loading
正在整理内容
页面内容马上就来,先让这盏小灯亮一下。
Go panic recover 源码解析:recover 为什么只能捕获当前 goroutine 的 panic | xiyingqingGo panic recover 源码证明
Go 版本:go1.23.3 darwin/arm64
源码目录:/usr/local/go/go1.23.3/src/runtime
需要掌握的技术内容目录
panic 在 runtime 里对应 gopanic
- 每个 goroutine 的 runtime 结构是
g
g 自己保存 _panic 链和 _defer 链
recover 只检查当前 goroutine 的 _panic
- 未被 recover 的 panic 会进入
fatalpanic
- panic 不跨 goroutine 传播,但未恢复的 panic 会终止整个进程
技术实现
goroutine 的运行时结构
Go runtime 里,每个 goroutine 都有一个 g 结构。关键字段在:
/usr/local/go/go1.23.3/src/runtime/runtime2.go:422
type g struct {
stack stack
_panic *_panic
_defer *_defer
m *m
...
}
| 字段 | 含义 |
|---|
stack | 这个 goroutine 自己的栈区间 |
_panic | 当前 goroutine 正在处理的 panic 链 |
_defer | 当前 goroutine 注册的 defer 链 |
m | 当前运行它的 OS 线程 |
也就是说,panic 和 defer 都是挂在当前 goroutine 的 g 上,不是挂在全局变量上。
panic 入口:gopanic
预定义函数 panic() 最终会进入 runtime 的 gopanic:
/usr/local/go/go1.23.3/src/runtime/panic.go:735
这说明 panic 从一开始就绑定到了当前 goroutine。
p.start 里会把当前 panic 挂到当前 goroutine 上:
/usr/local/go/go1.23.3/src/runtime/panic.go:838
p.link = gp._panic
gp._panic = &p
这一步能证明:panic 链是当前 g 自己的字段。
panic 如何执行 defer
gopanic 接着循环调用当前 panic 可见的 defer:
/usr/local/go/go1.23.3/src/runtime/panic.go:780
for {
fn, ok := p.nextDefer()
if !ok {
break
}
fn()
}
nextDefer 会从当前 goroutine 上找 defer:
/usr/local/go/go1.23.3/src/runtime/panic.go:856
然后检查当前 goroutine 的 _defer:
if d := gp._defer; d != nil && d.sp == uintptr(p.sp) {
...
popDefer(gp)
return fn, true
}
如果当前栈帧没有 defer,它会继续找上一个栈帧:
/usr/local/go/go1.23.3/src/runtime/panic.go:922
if !p.nextFrame() {
return nil, false
}
nextFrame 也是用当前 g 初始化栈展开器:
/usr/local/go/go1.23.3/src/runtime/panic.go:934
gp := getg()
u.initAt(..., gp, ...)
所以 panic 展开的是当前 goroutine 的调用栈,不会去扫描别的 goroutine。
recover 为什么不能跨 goroutine
recover() 对应 runtime 里的 gorecover:
/usr/local/go/go1.23.3/src/runtime/panic.go:1008
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
recover 先通过 getg() 拿当前 goroutine。
- 它只看当前 goroutine 的
gp._panic。
- 如果当前 goroutine 没有正在处理的 panic,就返回
nil。
- 它还要求
argp 匹配,确保 recover 是在正确的 defer 调用位置执行的。
因此,main goroutine 里的 defer recover 捕不到子 goroutine 的 panic。因为 main goroutine 的 gp._panic 是空的,panic 挂在子 goroutine 的 g._panic 上。
main goroutine 的 g
├── _panic: nil
└── _defer: main 的 defer recover
child goroutine 的 g
├── _panic: panic("boom")
└── _defer: child 自己的 defer 链
recover 只查当前这一个 g,不会从 main 的 g 跳到 child 的 g。
recover 成功后 runtime 怎么继续执行
当 gorecover 成功时,会把当前 _panic 标记成已恢复:
/usr/local/go/go1.23.3/src/runtime/panic.go:1018
之后 nextDefer 发现 p.recovered,会调用:
/usr/local/go/go1.23.3/src/runtime/panic.go:864
recovery 会整理当前 goroutine 的 panic 链,并把控制流恢复到合适位置:
/usr/local/go/go1.23.3/src/runtime/panic.go:1109
这也说明 recover 是当前 goroutine 栈展开过程中的控制流恢复,不是跨 goroutine 的异常捕获。
未 recover 的 panic 为什么会终止进程
如果 gopanic 把所有 defer 都执行完了,仍然没有 recover,源码会走到:
/usr/local/go/go1.23.3/src/runtime/panic.go:804
/usr/local/go/go1.23.3/src/runtime/panic.go:1263
/usr/local/go/go1.23.3/src/runtime/panic.go:1293
这就是为什么某个子 goroutine 未 recover 的 panic,不是只让这个 goroutine 结束,而是让整个 Go 进程退出。
完整链路
panic()
↓
runtime.gopanic
↓
gp := getg()
↓
创建 _panic,挂到 gp._panic
↓
沿当前 gp 的调用栈找 defer
↓
执行当前 gp._defer 链上的 defer
↓
defer 中调用 recover()
↓
runtime.gorecover 仍然只检查当前 gp._panic
↓
如果 recover 成功:标记 p.recovered,进入 recovery 恢复控制流
↓
如果所有 defer 执行完都没 recover:fatalpanic -> exit(2)
和实验结果对应
之前的程序里,main goroutine 写了:
defer func() {
recover()
}()
go func() {
panic("panic from child goroutine")
}()
- 子 goroutine 执行
panic,gopanic 里的 getg() 拿到的是子 goroutine 的 g。
_panic 被挂到子 goroutine 的 gp._panic。
- runtime 只执行子 goroutine 的 defer 链。
- main goroutine 的 defer 不在子 goroutine 的栈上,也不在子 goroutine 的
_defer 链上。
- 子 goroutine 没有 recover,最终进入
fatalpanic,整个进程 exit(2)。
因此,“panic 只沿当前 goroutine 的调用栈展开;recover 也只能恢复当前 goroutine 中正在展开的 panic”这个结论,可以从 gopanic、gorecover、g._panic、g._defer 这几处源码直接证明。