暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【Golang】图解panic & recover

幼麟实验室 2020-06-25
397


 看到套来套去的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是否被终止。

          所以要终止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发生的顺序一致。




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





                    文章转载自幼麟实验室,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论