Go 语言中 panic 和 recover 搭配使用

2023年 10月 9日 127.6k 0

本次主要聊聊 Go 语言中关于 panic 和 recover 搭配使用 ,以及 panic 的基本原理

最近工作中审查代码的时候发现一段代码,类似于如下这样,将 recover 放到一个子协程里面,期望去捕获主协程的程序异常

图片图片

看到此处,是否会想这段代码在项目中是想当然写出来的吧,然而平日中,大多问题是出现在认知偏差上,那么本次,我们就来消除一下这个认知偏差

关于 Go 语言中显示的使用 panic 的地方不多,一般 panic ,基本上会出现在咱们程序出现异常退出的时候

例如访问了空指针里面的值,则会 panic 报错无效的内存地址,又例如访问量数组中不存在的数组所索引,或者切片索引,那么会报错 panic 数组越界等等

可是碰到这些 panic 的时候,实际上我们并不期望当前的服务直接挂掉,而是期望这个异常能够被识别,且不影响程序其他部分的模块运行

正常捕获异常

在 Go 中可以将 defer 和 recover 进行搭配使用,可以捕获和处理大部分的异常情况,例如可以这样

图片图片

这里可以看到,recover 捕获异常和发生异常的部分是在同一个协程中,实验证明是可以正常捕获并且处理异常

并没有捕获到异常

  • 直接不做显示的 recover,自然 panic 程序崩溃会如期而至,此处我们显示的使用 panic 函数来制造恐慌
  • func main() {
       log.SetFlags(log.Lshortfile)
    
       panic("panic coming...")
    
    }

    图片图片

  • 不使用 defer 来进行处理
  • func main() {
       log.SetFlags(log.Lshortfile)
        if err := recover(); err != nil {
         log.Println("recover panic : ", err)
        }
       panic("panic coming...")
    
    }

    图片图片

    自然 recover 函数是在 panic 调用之前就已经执行,此时是还没有异常需要捕获和恢复的,待程序运行到 panic 处的时候,实际上并没有没有处理程序崩溃的异常

    结果,仍然是程序崩溃

  • 当然,还有文章开头提到的出现 panic 的位置和捕获和处理程序崩溃异常的位置不在同一个协程,自然也是没法捕获到的,这一点需要注意,其他的语言可能不是这样,但是 Go 中是这样的
  • panic 基本原理

    看了上述现象,实际上还是对知识点理解得不够,使用的时候想当然了,就像使用 defer 一样,如果对他不够了解的话,使用的时候,确实会出现一些奇奇怪怪的现象,对于 defer 的使用可以查看文末的文章地址

  • panic 函数和 recover 函数,Go 源码builtinbuiltin.go中可以看到注释
  • 图片图片

    注释中有说关于 panic 和 recover 的使用是作用于当前协程的,因此我们使用的时候,如果跨协程教程使用,自然不会达到我们期望的效果

  • 继续查看关于 panic 的源码,实际上是一个结构,放到 defer 结构里面的一个指针,源码位置:runtimeruntime2.go
  • 图片图片

    _panic 的结构如下:

    type _panic struct {
       argp      unsafe.Pointer
       arg       interface{}
       link      *_panic
       pc        uintptr
       sp        unsafe.Pointer
       recovered bool
       aborted   bool
       goexit    bool
    }

    上述两个结构表达的意思是,程序中出现 panic 的时候,实际上都会创建一个 _panic 结构,这个 _panic 结构里面存储了当前程序崩溃的一些必要信息,如下:

  • argp
  • 是一个 unsafe.Pointer 类型的成员,指向 defer 调用参数的指针

  • arg
  • 出现 panic 的原因,如果我们显示调用 panic,那么就是我们填入 panic 函数中的参数,例如上述的 panic coming ...

  • link
  • 是一个指针,指向上一个,最近的一个 _panic 结构的地址,实际上此处就可以看到这个指针对应的是一个链表,一个又多个 _panic 结构组成的链表

    图片图片

  • recovered
  • panic 是否已经处理完毕,即当前的这个 panic 是否是已经被 recover 了

  • aborted
  • 表示当前的 panic 是否被中止

  • 对于 pc 和 sp 自然就是我们熟知的 pc 通用寄存器,在汇编中是指向当前运行指令的下一条指令,sp 则是栈指针 stack pointer,用于入栈和出栈的
  • 我们知道运行函数的时候需要入栈,运行完毕之后需要出栈

    源码中的 runtime.gopanic

    那么我们继续来阅读源码,上述看到 sp 和 pc ,那么我们就简单写一个 panic 的代码来看看汇编到底是怎么执行的,不用担心看不懂,我们只需要看关键词就行

    还是上面的程序

    图片图片

    程序运行的时候可以执行 go tool compile -S main.go

    可以看到汇编代码,可能其他的看不懂,但是我们可以看到如下关键词

    图片图片

    • log.(*Logger).SetFlags(SB) 即是执行到我们调用 log 去设置参数
    • 程序走到 panic 函数的时候,实际上是执行了 runtime.gopanic 函数,我们一起看看源码

    图片图片

    代码中可以看到 p.recovered 逻辑下的关于 recover 的逻辑被删除掉了,在文章的后面会继续说到,当前我们先关注 panic 的事项

    runtime.gopanic 程序的逻辑大体是这样的

  • 获取当前 协程 的指针
  • 初始化一个 _panic 结构 p,并将当前协程上对应的数据赋值给到 p 上,且将 当前协程 _panic 挂到 link 上
  • 进入循环后,拿到当前协程的 _defer 数据
  • 查看 _defer 指针数据 中是否有 defer 调用,如果有则执行
  • 处理完基本逻辑之后,打印 panic 信息,例如我们 demo 中的 panic coming ... 信息
  • 最终退出程序
  • Xdm 可以看上图,自己捋一捋逻辑就清晰了

    接着,我们来看

    fatalpanic

    图片图片

    通过 runtime.gopanic 我们可以看到 fatalpanic 函数基本上就是做一个收尾工作了,如果上述程序处理完毕之后, fatalpanic 校验到 panic 是需要 recover 的,那么就打印 [recovered]

    打印的这个信息是由 上图中 printpanics 完成的

    图片图片

    这下知道 panic 是如何去执行的了,那么对于现在来研究 recover 是如何落实的

    recover

    还是同一个例子,咱们将 defer 部分的代码注打开,来继续看看效果

    func main() {
       log.SetFlags(log.Lshortfile)
    
       defer func() {
          if err := recover(); err != nil {
             log.Println("recover panic : ", err)
          }
       }()
    
       panic("panic coming...")
    
    }

    自然效果是我们期望的,捕获到了异常,且处理了

    图片图片

    继续打印汇编来查看一下关键词,是否有我们期望的函数出现

    图片图片

    图片图片

    此处我们可以看到,实际 Go 中调用了多个函数

  • runtime.gorecover
  • main.main.opendefer
  • log.(*Logger).SetFlags
  • runtime.gopanic
  • runtime.deferreturn
  • 自然明眼人都看的出现,关键的函数实现自然是 runtime.gorecover ,那么我们来一探究竟

    runtime.gorecover

    图片图片

    查看源码我们可以知道, runtime.gorecover 实际上就是根据当前协程的 _panic 结构数据来判断是否需要恢复,如果需要则将 p.recovered = true

    自然在这里将当前协程的数据修改掉,正是为了后续执行 runtime.gopanic 的时候提供保障, runtime.gopanic 执行的时候就会去判断和处理这个 p.recovered

    前文中提到的关于 runtime.gopanic 中 处理 p.recovered 的逻辑是这样的

    图片图片

    图片图片

  • 如上可以看到 runtime.gorecover 去对 p.recovered 设置是否恢复
  • runtime.gopanic 中校验 p.recovered 已处理,则执行 recovery 函数
  • recovery 函数中去处理对应的寄存器的值去维护上下文
  • 最后我们可以看到最终调用 gogo 函数跳回原来调用的位置
  • 因此,当我们在同一个协程中出现了 panic,且在同一个协程中去使用 defer 来配合 recover 来进行捕获异常和处理异常,就可以得以实现,看到这里,有没有觉得还是蛮简单的,不就是去对一个 p.recovered 进行配合处理吗

    自然,表面上是这样,其中对于寄存器的各种数据处理涉及的内容还是不少的,不过这不在我们今天聊的范畴中了

    总结

    至此,相信你已经知道了这些

  • 为什么 panic 和 defer ,recover 配合使用的时候要在同一个协程中了吧
  • 相信你还知道了 panic 和 recover 的处理流程
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论