前言
有这样一段代码:
func main() {
// 捕捉异常
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
go func() {
fmt.Println("start goroutine")
panic("error")
}()
// 等待
time.Sleep(time.Second * 100)
}
浅猜一下结果? 没错, 进程崩溃了, defer
没有捕捉到异常.
面对这个问题, 可能有的人想的是: "哦, defer
不能处理其他goroutine
的异常". 但我正巧闲得慌, 索性研究一下Go
中异常是如何处理的.
探究
Go
语言现在已经实现自举了, 所以其源码也是Go
, 不用去看C
了. 源码地址: https://github.com/golang/go. 以下源码基于分支release-branch.go1.18
因为当前主要研究的是异常的处理, 所以对协程的原理及defer
的调用不做过多探究.
结构体
在Go
中, 每个协程都有一个结构体来记录其运行信息, 这个结构体的名字是g
. 内容大致如下(定义在文件runtime.runtime2.go
):
type g struct {
// ...
// 当前协程已出发的异常链
_panic *_panic
// 记录当前协程的所有 defer 的链表
_defer *_defer
// ...
}
其他的字段都跳过, 只看_defer
字段, 也就是说在Go
中defer
和异常是记录在当前协程中的. 再回到开头的问题, 新启动的协程是没有defer
函数的, 自然也就无法捕捉到异常.
既然是研究异常处理, _panic
这个结构体自然也得瞅瞅了.
type _panic struct {
// defer 函数用的, 具体等到研究 defer的时候再看
argp unsafe.Pointer
// 调用 panic 函数时的参数
arg any
// 指向上一个 panic, 组成一个 panic 链表
// 因为在 panic 处理时, 可能再次发生异常
// 比如在 defer 函数中发生 panic
link *_panic
// 当前异常是否已经被 recover 处理
recovered bool
// 是否被强制终止
aborted bool
goexit bool
pc uintptr
sp unsafe.Pointer
}
其中pc
sp
goexit
三个字段, 在defer
中发生panic
, 然后上层defer
对异常进行了recover
时使用的, 具体作用我也没整太明白, 可看这次commit 以及这个 issuse
OK, 到这里, 我们和异常的结构体见了一面了, 但异常触发时是如何处理的呢?
处理流程
先上一张流程图来对异常的处理流程进行较为形象的表达.
从上图中, 可以基本看出处理的流程. 不过还是康一下源码吧(为了不影响篇幅, 只保留了部分内容).
你问我怎么知道调用了源码中的哪个函数? 使用命令go tool compile -N -l -S main.go
看一下咯.
// 处理异常的函数
func gopanic(e any) {
gp := getg()
// ...
// 创建新的异常并添加到协程的 panic 链表头
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
//...
for {
d := gp._defer
//...
done := true
// 调用 defer 函数
// 关于 openDefer, 在这里先按下不表
if d.openDefer {
done = runOpenDeferFrame(gp, d)
if done && !d._panic.recovered {
addOneOpenDeferFrame(gp, 0, nil)
}
} else {
p.argp = unsafe.Pointer(getargp())
d.fn()
}
//...
if done {
//...
// 将当前 defer 从协程的 defer 链中去掉
freedefer(d)
}
// 异常已经被处理啦
if p.recovered {
//...
// 恢复协程运行. 这里恢复的 recovery 方法不返回
mcall(recovery)
throw("recovery failed")
}
}
// 打印 panic 信息
preprintpanics(gp._panic)
// 进程终止
fatalpanic(gp._panic)
*(*int)(nil) = 0
}
// 当调用 recover 时触发
// 逻辑很简单, 就是从异常链表的头部将异常取出来
// 因为新的异常会放到 _panic 链表头, 所以这里拿到的是最新的异常
func gorecover(argp uintptr) any {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
// 标记当前异常已经被处理
p.recovered = true
return p.arg
}
// 当前没有异常
return nil
}
从gopanic
函数的异常处理流程来看, 异常在gorecover
中一旦被处理, 就会将p.recovered
标记为true
. 而外层一旦检测到其值为true
就会恢复运行. 看着貌似没什么问题哈, 但是还记不记得_panic
是一个链表呀? 这不是才处理了一个异常么? 如果有其他异常不就丢了么? 比如下面这种情况:
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer func() {
panic("error 2")
}()
panic("error")
}
没错, 我们在先调用的defer
中再次抛出异常, 此时, error
这个异常就没有啦. 现象是符合我们前面的分析的.
系统级异常
在运行时有一些异常是无法通过recover
捕获的. 比如调用throw
函数的错误, throw
函数源码如下:
func throw(s string) {
systemstack(func() {
print("fatal error: ", s, "\n")
})
gp := getg()
if gp.m.throwing == 0 {
gp.m.throwing = 1
}
// 直接强制停止
fatalthrow()
*(*int)(nil) = 0 // not reached
}
那么哪些操作会触发无法捕获的异常呢? 在系统实现上到处有调用throw
函数的, 比如:
map
的并发读写- 占内存耗尽, 比如递归太深
go
启动的函数为nil
OK, 至此, 虽然在源码层面没有分析的特别细致, 但是对异常的处理流程基本能做到心中有数啦