写点什么

谈谈 panic 和 recover 的原理

  • 2019-12-03
  • 本文字数:4926 字

    阅读完需:约 16 分钟

谈谈 panic 和 recover 的原理

4.4 panic 和 recover

这一节中我们将介绍 Go 语言中两个经常成对出现的关键字 panicrecover 的实现原理,我们在上一节关注的 defer 与这里介绍的两个关键字其实也有着比较大的关系,我们会在剩下的部分展开介绍相关的内容,没有阅读 上一节 的读者还是需要补充一下相关知识,这样才能更好地了解 panicrecover 关键字的原理。

__1. 概述

在具体介绍和分析 Go 语言中的 panicrecover 的实现原理之前,我们首先需要对它们有一些基本的了解;panicrecover 两个关键字其实都是 Go 语言中的内置函数,panic 能够改变程序的控制流,当一个函数调用执行 panic 时,它会立刻停止执行函数中其他的代码,而是会运行其中的 defer 函数,执行成功后会返回到调用方。



对于上层调用方来说,调用导致 panic 的函数其实与直接调用 panic 类似,所以也会执行所有的 defer 函数并返回到它的调用方,这个过程会一直进行直到当前 Goroutine 的调用栈中不包含任何的函数,这时整个程序才会崩溃,这个『恐慌过程』不仅会被显式的调用触发,还会由于运行期间发生错误而触发。


然而 panic 导致的『恐慌』状态其实可以被 defer 中的 recover 中止,recover 是一个只在 defer 中能够发挥作用的函数,在正常的控制流程中,调用 recover 会直接返回 nil 并且没有任何的作用,但是如果当前的 Goroutine 发生了『恐慌』,recover 其实就能够捕获到 panic 抛出的错误并阻止『恐慌』的继续传播。


概述这一小节的内容,大部分直接来自于 Go 语言的博客 Defer, Panic, and Recover,文章介绍了三种 Go 语言的常见关键字的常见使用场景。

__1.1. 常见使用

我们简单举两个例子简单了解一下 panicrecover 关键字的原理,先来看第一个例子:


func main() {    defer println("in main")    go func() {        defer println("in goroutine")        panic("")    }()
time.Sleep(1 * time.Second)}
// in goroutine// panic:// ...
复制代码


当我们运行这段代码时,其实会发现 main 函数中的 defer 语句并没有执行,执行的其实只有 Goroutine 中的 defer,这其实就印证了 Go 语言在发生 panic 时只会执行当前协程中的 defer 函数,这一点从 上一节 的源代码中也有所体现。


另一个例子就不止涉及 panicdefer 关键字了,我们可以看一下 recover 是如何让当前函数重新『走向正轨』的:


func main() {    defer fmt.Println("in main")    defer func() {        if err := recover(); err != nil {            fmt.Println(err)        }    }()
panic("unknown err")}
// unknown err// in main
复制代码


从这个例子中我们可以看到,recover 函数其实只是阻止了当前程序的崩溃,但是当前控制流中的其他 defer 函数还会正常执行。


在最后,我们需要知道的是可以在 defer 中连续多次调用 panic 函数,这是一个 Go 语言中 panic 比较有意思的现象:


func main() {    defer fmt.Println("in main")    defer func() {        panic("panic again")    }()
panic("panic once")}
// in main// panic: unknown err// panic: again// // goroutine 1 [running]:// main.main.func1()// ...
复制代码


当我们运行上述代码时,从打印出的结果中可以看到当前的函数确实经历了两次 panic,并且最外层的 defer 函数也能够正常执行

__2. 实现原理

既然已经介绍完了现象并且已经对 panicrecover 有了一定的了解,接下来我们就会从 Go 语言的源代码层面对上一节中谈到的现象一探究竟,这一节接下来的内容就是介绍这两个函数的实现原理了,作为 Go 语言中的关键字,我们还是会从编译期间和运行时两方面介绍它们。


panicrecover 关键字会在 编译期间 被 Go 语言的编译器转换成 OPANICORECOVER 类型的节点并进一步转换成 gopanicgorecover 两个运行时的函数调用。

__2.1. 数据结构

panic 在 Golang 中其实是由一个数据结构表示的,每当我们调用一次 panic 函数都会创建一个如下所示的数据结构存储相关的信息:


type _panic struct {    argp      unsafe.Pointer    arg       interface{}    link      *_panic    recovered bool    aborted   bool}
复制代码


  1. argp 是指向 defer 调用时参数的指针;

  2. arg 是调用 panic 时传入的参数;

  3. link 指向了更早调用的 _panic 结构;

  4. recovered 表示当前 _panic 是否被 recover 恢复;

  5. aborted 表示当前的 panic 是否被强行终止;


从数据结构中的 link 字段我们就可以推测出以下的结论 — panic 函数可以被连续多次调用,它们之间通过 link 的关联形成一个链表。

__2.2. 崩溃

首先了解一下没有被 recoverpanic 函数是如何终止整个程序的,我们来看一下 gopanic 函数的实现


func gopanic(e interface{}) {    gp := getg()    // ...    var p _panic    p.arg = e    p.link = gp._panic    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
for { d := gp._defer if d == nil { break }
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
p.argp = unsafe.Pointer(getargp(0)) reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) p.argp = nil
d._panic = nil d.fn = nil gp._defer = d.link
pc := d.pc sp := unsafe.Pointer(d.sp) freedefer(d) if p.recovered { // ... } }
fatalpanic(gp._panic) *(*int)(nil) = 0}
复制代码


我们暂时省略了 recover 相关的代码,省略后的 gopanic 函数执行过程包含以下几个步骤:


  1. 获取当前 panic 调用所在的 Goroutine 协程;

  2. 创建并初始化一个 _panic 结构体;

  3. 从当前 Goroutine 中的链表获取一个 _defer 结构体;

  4. 如果当前 _defer 存在,调用 reflectcall 执行 _defer 中的代码;

  5. 将下一位的 _defer 结构设置到 Goroutine 上并回到 3;

  6. 调用 fatalpanic 中止整个程序;


fatalpanic 函数在中止整个程序之前可能就会通过 printpanics 打印出全部的 panic 消息以及调用时传入的参数:


func fatalpanic(msgs *_panic) {    pc := getcallerpc()    sp := getcallersp()    gp := getg()    var docrash bool    systemstack(func() {        if startpanic_m() && msgs != nil {            atomic.Xadd(&runningPanicDefers, -1)
printpanics(msgs) } docrash = dopanic_m(gp, pc, sp) })
if docrash { crash() }
systemstack(func() { exit(2) })
*(*int)(nil) = 0 // not reached}
复制代码


fatalpanic 函数的最后会通过 exit 退出当前程序并返回错误码 2,不同的操作系统其实对 exit 函数有着不同的实现,其实最终都执行了 exit 系统调用来退出程序。

__2.3. 恢复

到了这里我们已经掌握了 panic 退出程序的过程,但是一个 panic 的程序也可能会被 defer 中的关键字 recover 恢复,在这时我们就回到 recover 关键字对应函数 gorecover 的实现了:


func gorecover(argp uintptr) interface{} {    p := gp._panic    if p != nil && !p.recovered && argp == uintptr(p.argp) {        p.recovered = true        return p.arg    }    return nil}
复制代码


这个函数的实现其实非常简单,它其实就是会修改 panic 结构体的 recovered 字段,当前函数的调用其实都发生在 gopanic 期间,我们重新回顾一下这段方法的实现:


func gopanic(e interface{}) {    // ...
for { // reflectcall
pc := d.pc sp := unsafe.Pointer(d.sp)
// ... if p.recovered { gp._panic = p.link for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } if gp._panic == nil { gp.sig = 0 } gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw("recovery failed") } }
fatalpanic(gp._panic) *(*int)(nil) = 0}
复制代码


上述这段代码其实从 _defer 结构体中取出了程序计数器 pc 和栈指针 sp 并调用 recovery 方法进行调度,调度之前会准备好 sppc 以及函数的返回值:


func recovery(gp *g) {    sp := gp.sigcode0    pc := gp.sigcode1
gp.sched.sp = sp gp.sched.pc = pc gp.sched.lr = 0 gp.sched.ret = 1 gogo(&gp.sched)}
复制代码


defer 一节中我们曾经介绍过 deferproc 的实现,作为创建并初始化 _defer 结构体的函数,它会将 deferproc 函数开始位置对应的栈指针 sp 和程序计数器 pc 存储到 _defer 结构体中,这里的 gogo 函数其实就会跳回 deferproc


TEXT runtime·gogo(SB), NOSPLIT, $8-4    MOVL    buf+0(FP), BX        // gobuf    MOVL    gobuf_g(BX), DX    MOVL    0(DX), CX        // make sure g != nil    get_tls(CX)    MOVL    DX, g(CX)    MOVL    gobuf_sp(BX), SP    // restore SP    MOVL    gobuf_ret(BX), AX    MOVL    gobuf_ctxt(BX), DX    MOVL    $0, gobuf_sp(BX)    // clear to help garbage collector    MOVL    $0, gobuf_ret(BX)    MOVL    $0, gobuf_ctxt(BX)    MOVL    gobuf_pc(BX), BX    JMP    BX
复制代码


这里的调度其实会将 deferproc 函数的返回值设置成 1,在这时编译器生成的代码就会帮助我们直接跳转到调用方函数 return 之前并进入 deferreturn 的执行过程,我们可以从 deferproc 的注释中简单了解这一过程:


func deferproc(siz int32, fn *funcval) {    // ...
// deferproc returns 0 normally. // a deferred func that stops a panic // makes the deferproc return 1. // the code the compiler generates always // checks the return value and jumps to the // end of the function if deferproc returns != 0. return0() // No code can go here - the C return register has // been set and must not be clobbered.}
复制代码


跳转到 deferreturn 函数之后,程序其实就从 panic 的过程中跳出来恢复了正常的执行逻辑,而 gorecover 函数也从 _panic 结构体中取出了调用 panic 时传入的 arg 参数。

__3. 总结

Go 语言中 panicrecover 的实现其实与 defer 关键字的联系非常紧密,而分析程序的恐慌和恢复过程也比较棘手,不是特别容易理解。在文章的最后我们还是简单总结一下具体的实现原理:


  1. 在编译过程中会将 panicrecover 分别转换成 gopanicgorecover函数,同时将 defer 转换成 deferproc 函数并在调用 defer 的函数和方法末尾增加 deferreturn 的指令;

  2. 在运行过程中遇到 gopanic 方法时,会从当前 Goroutine 中取出 _defer 的链表并通过 reflectcall 调用用于收尾的函数;

  3. 如果在 reflectcall 调用时遇到了 gorecover 就会直接将当前的 _panic.recovered 标记成 true 并返回 panic 传入的参数(在这时 recover 就能够获取到 panic 的信息);

  4. 在这次调用结束之后,gopanic 会从 _defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 recovery 方法进行恢复;

  5. recovery 会根据传入的 pcsp 跳转到 deferproc 函数;

  6. 编译器自动生成的代码会发现 deferproc 的返回值不为 0,这时就会直接跳到 deferreturn 函数中并恢复到正常的控制流程(依次执行剩余的 defer 并正常退出);

  7. 如果没有遇到 gorecover 就会依次遍历所有的 _defer 结构,并在最后调用 fatalpanic 中止程序、打印 panic 参数并返回错误码 2


整个过程涉及了一些 Go 语言底层相关的知识并且发生了非常多的跳转,相关的源代码也不是特别的直接,阅读起来也比较晦涩,不过还是对我们理解 Go 语言的错误处理机制有着比较大的帮助。

__4. Reference

__5. 其他

__5.1. 关于图片和转载

文章未经许可均禁止转载,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。


**本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/golang/keyword/golang-panic-recover.html


2019-12-03 15:101136

评论

发布
暂无评论
发现更多内容

2022 智能云边开源峰会|Kyligence 邀您“云”上相约

Kyligence

人工智能 云原生 边缘计算 开源峰会

Postman如何做接口测试:导入 swagger 接口文档

和牛

测试 Postman

大数据软件开发哪里的培训比较好

小谷哥

十年数智求索路,餐饮SaaS头部企业客如云如何走向盈利

ToB行业头条

盒马销量预测核心算法的技术演进

阿里技术

大数据 算法

我们还需要 SRE 吗?

Bytebase

DevOps SRE developer

网页制作常见问题网页的必要性与方法

Baklib

SPI:Java的高可扩展利器

华为云开发者联盟

Java 开发

未来源码丨会写代码的AI开源了!C语言写得比Codex还要好,掌握12种编程语言丨CMU

MobTech袤博科技

c 开源 AI

分布式系统架构设计

C++后台开发

数据库 分布式 后端开发 C/C++后台开发 C/C++开发

家电上云后,智能家居如何构建场景化应用

华为云开发者联盟

云计算 后端 IoT 智能家居

关于量化合约系统开发搭建及语言介绍

l8l259l3365

深度学习公式推导(2):激活函数与偏置

老崔说架构

INFINI 产品更新啦 20220815

极限实验室

elasticsearch infini gateway INFINI Labs 新版本/特性发布 INFINI Console

旅游吗?腾云驾雾的那种

天翼云开发者社区

Web Service 接口怎么测试

和牛

Python 接口 测试 Web Service

架构实战营毕业总结

Geek_Q

一不小心晋级“CCF国际AIOps挑战赛”决赛?

天翼云开发者社区

建设医共体,患者有“医”靠!

天翼云开发者社区

Go-Excelize API源码阅读(十一)—— GetActiveSheetIndex()

Regan Yue

Go 开源 源码刨析 8月日更 8月月更

如何管理您的知识库?

Geek_da0866

java培训班学习后怎样才能找到工作

小谷哥

EMAS Serverless搭建《私人云相册》小程序赢中秋好礼

移动研发平台EMAS

小程序 阿里云 Serverless 中秋节 云相册

前端培训中怎么提升开发技术水平?

小谷哥

零基础前端培训学习有用吗

小谷哥

谈谈 panic 和 recover 的原理_文化 & 方法_Draveness_InfoQ精选文章