“ 看到套来套去的panic和recover就心累?其实画一画,清晰明了~”
我们已经知道,当前执行的goroutine持有一个defer链表的头指针。其实它也有一个panic链表头指针。 图:panic链表panic链表链起来的是一个一个_panic结构体。和defer链表一样,发生新的panic时,也是在链表头上插入一个_panic结构体。而链表头上的panic就是当前正在执行的那一个。
下面,我们通过几个例子了解一下panic和recover的处理逻辑。
01
—
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函数没有正常结束的情况,就像下面这个例子。
02
—
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是否被终止。所以要终止panicA1,就是把它的_panic.aborted字段置为true。而且defer链表中A1这一项也要被移除。
此时,defer链表为空,paic处理流程来到了打印panic信息这一步。
panic:panicApanic:panicA1
注意panic打印异常信息时,会打印此时panic链表中剩余的所有链表项。不过,并不是从链表头开始,而是从链表尾开始,按照链表项的插入顺序逐一输出。所以这个例子才会先输出panicA,然后是panicA1。打印完异常信息后,程序退出。
好了,到目前为止,没有recover发生的panic处理逻辑就算梳理完了,理解这个过程的关键点有两个: 1. panic执行defer函数的方式,先标记,后移除,目的是为了终止之前工作的panic;
2. panic异常信息:所有还在panic链表上的链表项都会被输出,顺序与panic发生的顺序一致。
03
—
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。
图:recover发生前
函数A2执行时发生recover,其实,recover函数本身的逻辑很简单,它只做一件事,就是把当前执行的panic置为已恢复,也就是把它的_panic.recovered字段置为true,其它的都不管。
所以函数A2中recover发生后会把当前执行的panicA置为已恢复,然后recover函数的任务就完成了。函数A2会继续往下执行,直到A2结束。 图:recover发生后 其实在每个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是否大于零的这部分逻辑。 图:recover后跳出当前panic 通过sp,可以恢复到函数A的栈帧;通过pc,可以把指令地址恢复到判断r是否大于零这里。但是r就不能是0了,否则函数A就会重复执行。我们之前提过这个返回值被编译器保存在一个寄存器中,所以只要把它置为1就可以执行goto ret,跳转到deferreturn这里继续执行defer链表了。 图:跳出panicA后恢复到函数A 注意,函数A这里的deferreturn只负责执行函数A中注册的defer函数,是通过栈指针来判断的。 图:函数A继续执行deferreturn
我们这个例子中,跳转到A的deferreturn这里后,下一个链表项A1仍然是函数A注册的defer,所以,接下来会执行defer函数A1,A1结束后,defer链表为空,函数A结束。
这就是recover的基本流程,理解的关键有两点:1. 跳出当前panic处理流程以后要恢复到哪里,又是怎样恢复到那里的;2.要注意,在发生recover的函数正常返回以后,才会检测当前panic是否被恢复,然后才会删除被恢复的panic。
04
—
recover后同一函数又panic
如果发生recover的函数,在返回前再次panic,情况又会如何?
func A(){ defer A1() defer A2() panic("panicA")}func A2(){ p := recover() fmt.Println(p) //这里会正常执行输出“panicA” panic("panicA2")}
这个例子中,函数A发生panic,然后开始执行defer函数A2。在A2这里发生recover时,只会把当前panic也就是panicA置为已恢复,然后A2继续执行,再次发生panic时,会在panic链表头插入新的_panic,我们把它记为panicA2。现在它成为当前执行的panic了。 图:recover后,同一函数再次panic
当panicA2触发defer链表执行时,发现defer函数A2已经执行,所以把触发它执行的panicA终止掉。A2这一项也会从链表移除。
值得注意的是,由于A2没有正常返回,所以即使panicA已经被恢复了,也没有从链表中移除。 图:终止之前工作的panic
然后panicA2继续执行defer函数A1,A1中记录的_defer._panic指向panicA2。 图:panicA2继续执行defer链表
函数A1结束后,defer链表为空,接下来就要输出异常信息了。 图:defer链表执行完
对于链表中已经被恢复的panic,打印它的信息时会加上recovered标记,panic链表每一项都输出后程序退出。
panic:panicA[recovered]panic:panicA2
05
—
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继续执行。
图:recover后恢复到A2
因为A2注册的defer函数已经执行完了,所以函数A2返回。最终返回到哪里呢?回到panicA这里继续执行,因为A2的执行就是由panicA触发的。 图:A2结束后恢复到panicA
回到panicA这里,继续执行defer链表,接下来就轮到函数A1了。 图:panicA继续执行defer函数A1
等到A1执行结束,defer链表为空。输出panic链表上仅剩的panicA的异常信息之后程序就退出了。
06
—
recover调用限制
关于recover,还要强调最后一点,就是recover函数只能在defer函数中直接调用,不能通过另外的函数间接调用。这是语言实现层面的要求,不满足要求的recover调用,不会有任何效果。 图:recover调用限制
07
—
关于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的总体设计思想是一致的。