golang panic和recover

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处理逻辑就算梳理完了,理解这个过程的关键点有两个:

  1. panic执行defer函数的方式,先标记,后移除,目的是为了终止之前工作的panic;
  2. 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的基本流程,理解的关键有两点:

  1. 跳出当前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的总体设计思想是一致的。

本文转载于:

https://mp.weixin.qq.com/s/vcJ6TsnknaCoYhH6XZnNMw

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值