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

前言

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

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

热身

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

go 复制代码
package main
import "fmt"

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

运行结果:

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

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

panic是什么?

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

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

panic的结构

panic的底层源码如下

rust 复制代码
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中我们可以找到它

go 复制代码
func recover() any

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

使用工具找运行时的代码

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

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

go 复制代码
package main

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

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

我的是这个

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

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

也就是 runtime.gorecover()函数

真实源码

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

go 复制代码
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中被调用
相关推荐
PetterHillWater16 分钟前
Kimi-K2模型真实项目OOP重构实践
后端·aigc
Moonbit35 分钟前
月报 Vol.02:新增条件编译属性 cfg、#alias属性、defer表达式,增加 tuple struct 支持
后端·程序员·编程语言
Ray6640 分钟前
AviatorScript 表达式引擎
后端
回家路上绕了弯2 小时前
深度理解 Lock 与 ReentrantLock:Java 并发编程的高级锁机制
java·后端
咕噜分发企业签名APP加固彭于晏2 小时前
腾讯云eo激活码领取
前端·面试
Captaincc2 小时前
TRAE 首场 Meetup:8月16日,期待与你在北京相聚
前端·后端·trae
张元清2 小时前
避免 useEffect 严格模式双重执行的艺术
javascript·react.js·面试
肩塔didi3 小时前
用 Pixi 管理 Python 项目:打通Conda 和 PyPI 的边界
后端·python·github
岁忧3 小时前
(LeetCode 面试经典 150 题) 104. 二叉树的最大深度 (深度优先搜索dfs)
java·c++·leetcode·面试·go·深度优先
柏成3 小时前
基于 pnpm + monorepo 的 Qiankun微前端解决方案(内置模块联邦)
前端·javascript·面试