recover 没有捕获异常_【Golang】图解panic & recover

 看到套来套去的panic和recover就心累?其实画一画,清晰明了~

我们已经知道,当前执行的goroutine持有一个defer链表的头指针。其实它也有一个panic链表头指针。

1afd474de14dea2f876bad8040f449a0.png

图: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处理逻辑。

949cb5ad7c9bc7663a204729b745ef79.png

首先,会在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触发的。

ac2b1fc11d26b56575f7ed073f490083.png


把A2对应的_defer标记好以后,A2开始执行。这里函数A2能够正常结束,也就是没有发生panic或调用runtime.Goexit函数,所以A2这一项就会被移除,继续执行下个defer。

52fa168d3a14ba69e542a598c698537e.png


之所以要等到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。

efbb5aebbbb3f85942b743c69b6fc421.png


A1执行时,再次发生panic,同样要在panic链表头插入一个新的_panic,记为panicA1。现在这个panicA1成为当前执行的panic了。它同样会去执行defer链表,但是发现A1已经执行,并且触发它执行的并不是当前的panicA1,而是之前的panicA。

fa47867b77854a214b972ee8580500e8.png


这时会根据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这一项也要被移除。

32a138b9f6b7570bc17bdd1d417da291.png


此时,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。

85c2b2a44b3b0e550e02ee40392ddb54.png

图:recover发生前


函数A2执行时发生recover,其实,recover函数本身的逻辑很简单,它只做一件事,就是把当前执行的panic置为已恢复,也就是把它的_panic.recovered字段置为true,其它的都不管。
所以函数A2中recover发生后会把当前执行的panicA置为已恢复,然后recover函数的任务就完成了。函数A2会继续往下执行,直到A2结束。

0154f668e4eb6f6e547b8da4e5979d37.png

图:recover发生后 其实在每个defer函数执行完以后,panic处理流程都会检查当前panic是否被恢复了。这里A2结束后,panic处理流程发现panicA已经被恢复,所以就会把它从panic链表中移除。A2这一项也会从defer链表中移除,不过在移除前要保存_defer.sp和_defer.pc两个字段的值。

e1e7aa1c1a37c537023ab3e5caa419a3.png

接下来要做的,就是使用保存的sp和pc字段值跳出panicA处理流程,但是要怎么跳出来?又该恢复到哪里去呢?
我们知道,sp和pc是注册defer函数时保存的,对应到defer函数A2,sp就是函数A的栈指针,而pc就是调用deferproc(或deferprocStack)函数的返回地址。对应到下面这段伪指令中,就是函数A中判断r是否大于零的这部分逻辑。

0c9cde707822a3d0a272e468f2a71ddb.png

图:recover后跳出当前panic 通过sp,可以恢复到函数A的栈帧;通过pc,可以把指令地址恢复到判断r是否大于零这里。但是r就不能是0了,否则函数A就会重复执行。我们之前提过这个返回值被编译器保存在一个寄存器中,所以只要把它置为1就可以执行goto ret,跳转到deferreturn这里继续执行defer链表了。

2d9c3f0741ddf85fd95897d87595c0d7.png

图:跳出panicA后恢复到函数A 注意,函数A这里的deferreturn只负责执行函数A中注册的defer函数,是通过栈指针来判断的。

85b23c12f9ae293bc0c1080089925a04.png

图:函数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了。

00633ef27c8bce3a7fc83aa42e5813c1.png

图:recover后,同一函数再次panic
当panicA2触发defer链表执行时,发现defer函数A2已经执行,所以把触发它执行的panicA终止掉。A2这一项也会从链表移除。
值得注意的是,由于A2没有正常返回,所以即使panicA已经被恢复了,也没有从链表中移除。

1dd9cdaf19e962e7e3ad69629592263c.png

图:终止之前工作的panic
然后panicA2继续执行defer函数A1,A1中记录的_defer._panic指向panicA2。

98556f1062f95924599d23ba142f0a08.png

图:panicA2继续执行defer链表
函数A1结束后,defer链表为空,接下来就要输出异常信息了。

d03818855eab3c853630da6102b00650.png

图: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继续执行。

eb8d8aa90d3529323b06f2f89613cd2b.png

图:recover后恢复到A2
因为A2注册的defer函数已经执行完了,所以函数A2返回。最终返回到哪里呢?回到panicA这里继续执行,因为A2的执行就是由panicA触发的。

133e6add7a0c26d4754bf0335e9b53c4.png

图:A2结束后恢复到panicA
回到panicA这里,继续执行defer链表,接下来就轮到函数A1了。

ee7738e2314d5b87adaac252c44caa83.png

图:panicA继续执行defer函数A1
等到A1执行结束,defer链表为空。输出panic链表上仅剩的panicA的异常信息之后程序就退出了。

06

recover调用限制


关于recover,还要强调最后一点,就是recover函数只能在defer函数中直接调用,不能通过另外的函数间接调用。这是语言实现层面的要求,不满足要求的recover调用,不会有任何效果。

b323c45b46d79c355a885931348133f5.png

图: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的总体设计思想是一致的。

ecc399131045dc2b39a81c7478542f2c.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值