likes
comments
collection
share

Go高级之从源码分析recover函数为什么一定要在defer里面才生效

作者站长头像
站长
· 阅读数 18

前言

本文是探讨的是"recover函数为什么一定要在defer里面才生效"

此文章是个人学习归纳的心得, 如有不对, 还望指正, 感谢!

热身

请分析下面代码的运行结果

package main
import "fmt"

func main(){
   defer func(){
      func(){
         if err := recover(); err != nil {
               fmt.Println("A")
         }
      }()
   }()
   
   panic("demo") //触发惊恐
   fmt.Println("B")
}

运行结果:

Go高级之从源码分析recover函数为什么一定要在defer里面才生效

从运行结果,我们可以得知,recover并没有捕获到惊恐,而是由惊恐引发了程序崩溃

乍一看,可能这个例子与我们探索的目标可能没什么关系,但是这个例子考验了你对recover的认识,且听我徐徐道来

panic是什么?

可以类比其他语言中的异常,panic出现的时候,Go程序即将崩溃,至于为什么是“即将”,那是因为我们还可以通过recover函数来进行捕获,来挽救Go程序,使其正常运行,在Go语言中,忽略panic是一种有意识的行为。

recover函数就是为了捕获panic然后阻止程序崩溃的,要想了解recover,我们得先来认识panic,也就是我所谓的惊恐

panic的结构

panic的底层源码如下

type _panic struct {
   argp      unsafe.Pointer // 指向在 panic 期间运行的延迟调用的参数的指针;不能移动 - 已知由 liblink 处理
   arg       any            // panic 的参数,存储panic()函数传入值的
   link      *_panic        // 指向先前 panic 的链接
   pc        uintptr        // 如果绕过此 panic,返回到 runtime 中的位置
   sp        unsafe.Pointer // 和上面pc效果一样,但使用方法不一样
   recovered bool           //标志这个panic是否已经被recover()恢复
   aborted   bool           // 标志当前_panic是否被中止
   goexit    bool           // 标志当前实例是否由runtime.Goexit()产生的
}

具体定义在 src/runtime/runtime2.go

其中我们需要关注的是recovered属性,recover函数主要是通过修改这个属性来标志是否处理panic

值得一提的是,goexit属性,是用来标识当前goroutine是否为已退出的,Goexit函数产生的_panic会被标识,然后这个_panic就不会被recove函数捕获了。

当我们使用 panic("这是一个惊恐!") 的时候,就会产生一个_panic实例,值会存到 arg属性里面

recover函数是什么?

中文含义为“恢复”,是一个内置函数,用于捕获程序中的异常,使程序回到正常流程

recover()的源码

src/builtin/builtin.go中我们可以找到它

func recover() any

可惜的是,这并不是我们想要的,我们需要通过分析它在运行时的代码结构

使用工具找运行时的代码

我们可以使用go编译器自带的工具来从汇编进行分析

新建一个demo.go的文件,键入如下代码

package main

func main() {
   defer func() {
      recover()
   }()
}

然后是 go tool compile -S 文件路径,进行汇编展示。

我的是这个

Go高级之从源码分析recover函数为什么一定要在defer里面才生效

然后通过对应的行数找到对应的运算,如下图

Go高级之从源码分析recover函数为什么一定要在defer里面才生效

通过这个,我们找到了运行时的recover()的真实面貌

也就是 runtime.gorecover()函数

真实源码

src/runtime/panic.go中我们可以找到它,那我们也离揭开recovr()函数能捕获panic和为什么一定要在defer里面执行的谜题不远了

func gorecover(argp uintptr) any {
   gp := getg()
   p := gp._panic
   if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
      p.recovered = true
      return p.arg
   }
   return nil
}

看到这个函数时,我的第一反应是,为什么recover()没有传参,怎么gorecover函数要传参?

其实这个参数是编译运行的时候,解释器自动塞入的,塞的是指向调用recover()的父函数

先不要急,我们先看函数里面的结构,分析一下具体执行流程

  • 首先通过 内置函数getg()得到指向当前协程的指针。

  • 然后取出当前协程的_panic,也就是惊恐,如果没有惊恐,那就是nil

  • 接下来通过判断一系列条件之后,决定是否将_panic的recovered属性改为true并返回arg

前面我在介绍惊恐的原型——_panic的时候提到过recovered属性和arg属性,recovered是用来标识是否被recover处理过的,arg是用来存储panic()函数的传入参数的。

而关键点就在这个判断条件上:

  • 第一个条件是p != nil 也就是我们从这个协程中取出的_panic不为nil,这个协程确确实实出现了panic惊恐,因为recover就是用来处理panic用的

  • 第二个判断条件就是 !p.goexit,意思是这个panic不是由runtime.Goexit()产生的,Goexit函数在运行时会产生一个他自己使用的panic,为了避免被误处理,所以加了这个属性。

  • 第三个判断条件是 !p.recovered ,意思是当前panic没有被recoverv处理过,因为重复处理,没有意义了,所以在defer中多次调用recover,也只有第一次的会生效

  • 最后一个是argp == uintptr(p.argp)argp是编译运行的时候,解释器自动塞入的,塞的是指向调用recover()的父函数,而argp属性,我们也在前面讲_panic时也提到过,它是_panic的第一个属性,这个属性存放的是指向在 panic 期间运行的延迟调用的参数的指针,也就是当前recover函数所在的defer函数,当argp uintptr(p.argp)不相等的时候,也就是说明,当前recover函数不在defer里面,然后就没有进入if的内部语句,直接return nil

那这个判断recover在不在defer里面的意义在哪?

其实是这样的,在一个普通的协程中,recover不在defer中的话,那就是按顺序执行了,如果当时并没有panic的话,那recover就没有任何作用,毕竟这个函数的设计就是为了把快要崩溃的程序进行挽救,所以我们只有把这个函数放到defer中执行,它挽救快要崩溃的程序的功能才能发挥。

总结

recover运行的条件:

  • 该协程必须出现了panic
  • recover函数必须在和panic同级的defer中被调用