panic
我们已经知道当前执行的goroutine中有一个defer链表的头指针,其实它也有一个panic链表头指针,panic链表链起来的是一个个_panic结构体。和defer链表一样,发生panic时也是在链表头插入新的_panic结构体。链表头上的panic就是当前正在执行的那一个。
1
2
3
4
5
6
//src/runtime/runtime2.go#L403
type g struct {
...
_defer *_defer
_panic *_panic
}
看个例子:
1
2
3
4
5
6
7
func A() {
defer A1()
defer A2()
panic("panic A")
//code to do something
}
defer链表中注册了A1和A2后发生panic,panic后面的代码就不会再执行了,而是进入panic处理逻辑。
首先在panic链表上增加一项,它就是当前正在执行的panic。然后就该执行defer链表了,从头开始执行。
不过与函数正常执行defer函数有些不同,还记得_defer的结构体吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type _defer struct {
siz int32 // 参数和返回值共占多少字节
started bool //标记defer是否已经执行
heap bool //是否为堆分配
openDefer bool //go1.14增加
sp uintptr //记录注册这个defer函数栈指针,通过它函数可以判断自己注册的defer是否已经执行完了
pc uintptr //deferproc的返回地址
fn *funcval //要注册的函数 function value
_panic *_panic // panic that is running defer
link *_defer //链接到前一个注册的_defer结构体
fd unsafe.Pointer //go1.14增加
varp uintptr //go1.14增加
framepc uintptr//go1.14增加
}
panic执行defer时会先把_defer结构体的stated置为true,标记它已经开始执行,并把_panic字段指向当前正在执行的panic,表示这个defer是由这个panic触发的。
回到例子中,A2执行前也要先标记,如果A2能正常结束,这一项就会被移除,继续执行下一个defer。之所以这样设计是为了应对defer函数没有正常结束的情况。
1
2
3
4
5
func A1() {
//...
panic("panic A1")
//...
}
假如接下来要执行的defer函数A1中会再次发生panic。执行前同样标记它的started和_panic字段。当A1执行到panic时,这后面的代码也不会再执行了,然后在panic链表头插入一个新的panic,现在它称为当前正在执行的panic了。
插入 panicA1 g._defer -> A2 -> A1 g._panic -> panicA1 -> panicA
然后同样去执行defer链表但是发现defer A1已经执行并且它执行的并不是当前的panicA1,所以根据记录的panic指针找到对应的panic并把它标记为已终止。
g._defer -> A1 g._panic -> panicA1 -> panicA(标记已终止 )
_panic的结构体:
1
2
3
4
5
6
7
8
9
10
11
//src/runtime/runtime2.go#L943
type _panic struct {
argp unsafe.Pointer // defer的参数空间地址
arg interface{} // panic自己的参数
link *_panic // 链到之前发生的panic
pc uintptr // where to return to in runtime if this panic is bypassed
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
recovered bool // 表示panic是否被恢复
aborted bool // 标识panic是否被终止
goexit bool
}
注意:panic打印异常信息时会从链表尾开始,也就是按照panic发生的顺序逐个输出。
所以这里会先输出panicA,然后输出panicA1。
没有 recover 发生时,panic的处理逻辑就是这样。关键点有两个。
第一个是panic执行defer函数的方式:先标记后释放,目的是为了终止之前发生的panic。 第二个是panic异常信息的输出方式:所有在panic链表上的项都会被输出,顺序与panic发生的顺序一致。
接下来增加recover看看。
recover
recover 函数本身的逻辑很简单,它只做一件事:就是把当前执行的panic置为已恢复,也就是把它的recoverd字段置为true,其它的都不管。
看个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func A() {
defer A1()
defer A2()
...
panic("panicA")
}
func A1(){
}
func A2() {
p := recover()
rmt.Pritnln(p)
}
func main() {
A()
}
当执行到A中的panic时,当前goroutine中defer链表只有A1和A2,panic链表有一项:
1
2
g._defer -> A2 -> A1
g._panic -> panicA
panicA 触发 defer 执行,先执行函数 A2,然后执行到 A2 中的 recover,把当前执行的 panic 设置为已恢复, 然后recover函数的任务就执行完了。
1
g._panic -> panicA(recoverd=true, 已恢复)
程序继续往下走,直到A2结束。
其实在每个defer执行结束完以后,panic处理流程都会检查当前panic是否被它恢复了。此时发现panic已经被恢复,就会把它从链表中移除, A2这一项也会从defer链表中移除。
1
2
g._defer -> A1
g._panic -> nil
在发生panic时,实际上会调用runtime.gopanic函数,它负责添加链表项,并执行defer链表。
需要注意的是:在发生recover函数正常返回以后才会进入到检测panic是否被恢复的流程,然后才能删除被恢复的panic。
Go1.14版本以前panic和recover的基本处理流程就是这样,由于Go1.14中使用了open coded defer 导致panic执行defer链表时,不能如同之前这版轻松,需要通过栈扫描来找到未注册到defer链表中的defer函数,但是panic和recover的设计思想在这几个版本中都是一致的。