panic和recover

文章目录

  • panic和recover
    • panic
    • panic之后又panic
    • recover
    • recover后同一函数又panic
    • recover后恢复到哪里
    • recover调用限制
    • 关于open coded defer

我们已经知道,当前执行的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       int32started   bool    // panic执行defer时会把它标记为truesp        uintptr pc        uintptrfn        *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.Pointerarg       interface{}link      *_panicrecovered boolaborted   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

golang panic和recover相关推荐

  1. golang panic和recover 捕获异常

    func panic(interface{})和func recover() interface{}是Golang中用于错误处理的两个函数. panic的作用就是抛出一条错误信息,从它的参数类型可以看 ...

  2. Golang中的panic和recover(捕获异常)

    Golang中的panic和recover(捕获异常) 参考文章: (1)Golang中的panic和recover(捕获异常) (2)https://www.cnblogs.com/zhzhlong ...

  3. golang中的panic和recover

    golang中的panic需要recover捕获,不然程序就会挂掉 package mainimport "fmt"func main() {f1()fmt.Println(&qu ...

  4. Golang的Panic和Recover

    什么是 panic? 在 Go 语言中,程序中一般是使用错误来处理异常情况.对于程序中出现的大部分异常情况,错误就已经够用了. 但在有些情况,当程序发生异常时,无法继续运行.在这种情况下,我们会使用  ...

  5. golang panic recover return defer的逻辑顺序问题

    package mainimport "fmt"//验证golang return defer recover 之间的顺序关系func main() {defer func() { ...

  6. Golang中panic与recover的实现原理

    今天我们讲讲golang中panic异常,以及recover对异常的捕获,由于panic.recover.defer之间非常亲密,所以今天就放在一起讲解,这里会涉及到一些defer的知识,有兴趣可以看 ...

  7. Golang 错误捕获 Panic 与 Recover

    Golang 错误捕获 Panic 与 Recover,我抓住你了,Error Golang轻松学习 文章目录 Golang 错误捕获 Panic 与 Recover,我抓住你了,Error 一.Go ...

  8. Golang——error处理及panic、recover使用的正确姿势

    异常就是程序出现了不正常的情况,会导致程序非正常停止,而异常处理就是针对非正常停止的情况,给出异常时的处理方式.语法错误不算异常体系中 error: error是一个接口,作用是返回程序异常的信息,e ...

  9. 在golang中defer、panic与recover的作用

    package mainimport "fmt"func main() {var s strings = "panic"fmt.Printf("a的初 ...

最新文章

  1. Linux下如何执行Shell脚本
  2. C/C++笔试、面试题
  3. [转]Open Data Protocol (OData) Basic Tutorial
  4. java predicate原理_Java Predicate
  5. 波卡生态项目Polkalokr将在Polkastarter进行IDO
  6. 机器学习-吴恩达-笔记-12-推荐系统
  7. php清空html_PHP清除html格式的代码
  8. 54.原型3 移动端选择器 (未完)
  9. 微软 Windows 再度“围剿” Google Chrome
  10. [转]wxParse-微信小程序富文本解析组件
  11. 关于Adodb.Stream 的使用说明
  12. 任意切换线程的工具类
  13. 51Nod1344走格子
  14. laravel常用拓展库
  15. 进程调度算法-先来先服务、最短作业优先调度算法和高响应比优先调度算法
  16. 高校ACM题库(转载)
  17. 在Ubuntu上安装D-link DWA-131驱动
  18. novatel中DGPS和RTK以及ppp的terrraStar-x的记录
  19. app自动化之monkey测试
  20. Xmas snow for Mac(圣诞桌面装饰软件)

热门文章

  1. malloc(): corrupted top size 解决
  2. graylog+kafka+zookeeper(单机测试及源码),graylog组件部署,查找问题分析(一)
  3. macOS 视频格式转换器 MacX Video Converter Pro
  4. 印象笔记、为知笔记、有道云笔记使用比较
  5. 基于机器视觉的移动消防机器人(二)--详细设计
  6. 稻城亚丁神州租车自驾游,一生一定要去一次的地方
  7. 台湾华夏堂古代玉器收藏家~件件珍贵难得一见
  8. 【SoC FPGA】外设PIO按键点灯
  9. 后缀数组(倍增)学习记录,我尽可能详细的讲了
  10. win32应用程序内存不足