panic和recover
文章目录
我们已经知道,当前执行的goroutine持有一个defer链表的头指针。其实它也有一个panic链表头指针。
panic链表链起来的是一个一个_panic结构体。和defer链表一样,发生新的panic时,也是在链表头上插入一个_panic结构体。而链表头上的panic就是当前正在执行的那一个。
panic
func A(){
defer A1()
defer A2()
panic("panicA")
fmt.Println("这里不会被执行")
}
func A2(){
fmt.Println("A2正常结束")
}
func A1(){
fmt.Println("A1正常结束")
}
这个例子中,函数A注册两个defer函数A1和A2之后发生panic。panic发生前,defer链表中已经注册了A1和A2,我们同样用函数名作为区分标记。发生panic后,它后面的代码就不会执行了,而是进入panic处理逻辑。
首先,会在panic链表头处增加一项,我们把它记为panicA
,现在它就是当前执行的panic。然后就该执行defer链表了,从defer链表头开始执行,不过与函数正常流程执行defer有些许不同,还记得_defer结构体的内容吗?(Go1.12版本)
type _defer struct {
siz int32
started bool // panic执行defer时会把它标记为true
sp uintptr
pc uintptr
fn *funcval
_panic *_panic // 记录触发defer执行的_panic指针
link *_defer
}
panic执行到一个defer时,会先把它的_defer.started
置为true,标记它已经开始执行;并且会把_defer._panic
字段指向当前执行的panic,表示这个defer是由这个panic触发的。
把A2对应的_defer标记好以后,A2开始执行。这里函数A2能够正常结束,也就是没有发生panic或调用runtime.Goexit函数,所以A2这一项就会被移除,继续执行下个defer。
之所以要等到defer函数正常返回以后再移除对应的defer链表项,主要是为了应对defer函数没有正常结束的情况,就像下面这个例子。
panic之后又panic
func A(){
defer A1()
panic("panicA")
}
func A1(){
fmt.Println("A1再次panic")
panic("panicA1")
}
函数A中panic发生后,panic链表增加一项,记为panicA。然后就要执行defer链表了,设置A1对应的_defer.started与_defer._panic字段,然后调用函数A1。
A1执行时,再次发生panic,同样要在panic链表头插入一个新的_panic,记为panicA1。现在这个panicA1成为当前执行的panic了。它同样会去执行defer链表,但是发现A1已经执行,并且触发它执行的并不是当前的panicA1,而是之前的panicA。
这时会根据A1这里记录的_panic指针,找到对应的_panic,并把它标记为已终止。怎么标记?那就要把_panic结构体展开来看看了。
type _panic struct {
argp unsafe.Pointer
arg interface{}
link *_panic
recovered bool
aborted bool
}
- argp 用来存储panic正在执行的defer函数的参数空间地址;
- arg 则是panic函数自己的参数;
- link自然是链到上一个_panic结构体;
- recovered 标识这个panic是否被恢复;
- aborted 标识这个panic是否被终止。
所以要终止panicA,就是把它的_panic.aborted字段置为true。而且defer链表中A1这一项也要被移除。
此时,defer链表为空,paic处理流程来到了打印panic信息这一步。
panic:panicA
panic:panicA1
注意panic打印异常信息时,会打印此时panic链表中剩余的所有链表项。不过,并不是从链表头开始,而是从链表尾开始,按照链表项的插入顺序逐一输出。所以这个例子才会先输出panicA,然后是panicA1。打印完异常信息后,程序退出。
好了,到目前为止,没有recover发生的panic处理逻辑就算梳理完了,理解这个过程的关键点有两个:
- panic执行defer函数的方式,先标记,后移除,目的是为了终止之前工作的panic;
- panic异常信息:所有还在panic链表上的链表项都会被输出,顺序与panic发生的顺序一致。
recover
接下来我们增加recover看看是什么情况。下面这个例子中,函数A里注册了两个defer函数,并且会发生panic。而defer函数A2中会执行recover。
func A(){
defer A1()
defer A2()
panic("panicA")
}
func A2(){
p := recover()
fmt.Println(p) //这里会正常执行输出“panicA”
}
函数A中panic发生时,当前goroutine中defer链表已经注册了A1和A2。然后panic链表增加一项,记为panicA。panic触发defer链表执行,先执行函数A2。
函数A2执行时发生recover,其实,recover函数本身的逻辑很简单,它只做一件事,就是把当前执行的panic置为已恢复,也就是把它的_panic.recovered字段置为true,其它的都不管。
所以函数A2中recover发生后会把当前执行的panicA置为已恢复,然后recover函数的任务就完成了。函数A2会继续往下执行,直到A2结束。
其实在每个defer函数执行完以后,panic处理流程都会检查当前panic是否被恢复了。这里A2结束后,panic处理流程发现panicA已经被恢复,所以就会把它从panic链表中移除。A2这一项也会从defer链表中移除,不过在移除前要保存_defer.sp和_defer.pc两个字段的值。
接下来要做的,就是使用保存的sp和pc字段值跳出panicA处理流程,但是要怎么跳出来?又该恢复到哪里去呢?
我们知道,sp和pc是注册defer函数时保存的,对应到defer函数A2,sp就是函数A的栈指针,而pc就是调用deferproc(或deferprocStack)函数的返回地址。对应到下面这段伪指令中,就是函数A中判断r是否大于零的这部分逻辑。
通过sp,可以恢复到函数A的栈帧;通过pc,可以把指令地址恢复到判断r是否大于零这里。但是r就不能是0了,否则函数A就会重复执行。我们之前提过这个返回值被编译器保存在一个寄存器中,所以只要把它置为1就可以执行goto ret,跳转到deferreturn这里继续执行defer链表了。
注意,函数A这里的deferreturn只负责执行函数A中注册的defer函数,是通过栈指针来判断的。
我们这个例子中,跳转到A的deferreturn这里后,下一个链表项A1仍然是函数A注册的defer,所以,接下来会执行defer函数A1,A1结束后,defer链表为空,函数A结束。
这就是recover的基本流程,理解的关键有两点:
- 跳出当前panic处理流程以后要恢复到哪里,又是怎样恢复到那里的;
2.要注意,在发生recover的函数正常返回以后,才会检测当前panic是否被恢复,然后才会删除被恢复的panic。
recover后同一函数又panic
如果发生recover的函数,在返回前再次panic,情况又会如何?
func A(){
defer A1()
defer A2()
panic("panicA")
}
func A2(){
p := recover()
fmt.Println(p) //这里会正常执行输出“panicA”
panic("panicA2")
}
当panicA2触发defer链表执行时,发现defer函数A2已经执行,所以把触发它执行的panicA终止掉。A2这一项也会从链表移除。
值得注意的是,由于A2没有正常返回,所以即使panicA已经被恢复了,也没有从链表中移除。
然后panicA2继续执行defer函数A1,A1中记录的_defer._panic指向panicA2。
函数A1结束后,defer链表为空,接下来就要输出异常信息了。
对于链表中已经被恢复的panic,打印它的信息时会加上recovered标记,panic链表每一项都输出后程序退出。
panic:panicA[recovered]
panic:panicA2
recover后恢复到哪里
这个例子是为了加深对recover的理解,这一次我们结合函数调用关系弄清楚recover发生后,程序究竟会恢复到哪里。
func A(){
defer A1()
defer A2()
panic("panicA")
}
func A1(){
fmt.Println("A1正常执行")
}
func A2(){
defer B1()
panic("panicA2")
}
func B1(){
p := recover()
fmt.Println(p)//这里正常输出"panicA2"
}
- 函数A发生panic,实际上会调用gopanic函数来处理添加panic链表项与执行defer等工作。我们把这个panic记为panicA。
- panicA会执行A的defer函数A2。在A2执行时又注册了defer函数B1,然后再次发生panic,所以函数A2会调用gopanic来处理panicA2。
- panicA2会去执行defer链表,所以接下来会调用B1。
- B1执行时调用recover函数把panicA2置为已恢复。
- B1结束后,panicA2被移除,程序恢复到函数A2这里的deferreturn继续执行。
因为A2注册的defer函数已经执行完了,所以函数A2返回。最终返回到哪里呢?回到panicA这里继续执行,因为A2的执行就是由panicA触发的。
回到panicA这里,继续执行defer链表,接下来就轮到函数A1了。
等到A1执行结束,defer链表为空。输出panic链表上仅剩的panicA的异常信息之后程序就退出了。
recover调用限制
关于recover,还要强调最后一点,就是recover函数只能在defer函数中直接调用,不能通过另外的函数间接调用。这是语言实现层面的要求,不满足要求的recover调用,不会有任何效果。
关于open coded defer
Go1.14版本以前,panic和recover的基本流程就是这样。但是,由于1.14中使用了open coded defer,在函数内部展开调用的defer函数并没有注册到defer链表,导致panic执行defer链表时不能像之前这般轻松。
1.14版本中panic处理流程要在执行defer链表前先进行栈扫描,把第一个open codeed defer注册到链表中正确的位置。然后开始执行defer链表。而且每次都要判断_defer.openCoded的值,如果为true,就通过_defer记录的信息拿到所属函数中open coded defer的相关信息,然后按照正确的顺序执行。具体过程相当繁琐,但是panic和recover的总体设计思想是一致的。
本文转载于: