Go 语言中 panic 和 recover 搭配使用

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

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

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

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

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

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

正常捕获异常

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

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

并没有捕获到异常

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

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

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

结果,仍然是程序崩溃

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

panic 基本原理

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

  1. panic 函数和 recover 函数,Go 源码builtin\builtin.go中可以看到注释

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

  1. 继续查看关于 panic 的源码,实际上是一个结构,放到 defer 结构里面的一个指针,源码位置:runtime\runtime2.go

_panic 的结构如下:

type _panic struct {argp      unsafe.Pointerarg       interface{}link      *_panicpc        uintptrsp        unsafe.Pointerrecovered boolaborted   boolgoexit    bool
}

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

  1. argp

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

  1. arg

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

  1. link

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

  1. recovered

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

  1. aborted

表示当前的 panic 是否被中止

  1. 对于 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 程序的逻辑大体是这样的

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

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 中调用了多个函数

  1. runtime.gorecover
  1. main.main.opendefer
  1. log.(*Logger).SetFlags
  1. runtime.gopanic
  1. runtime.deferreturn

自然明眼人都看的出现,关键的函数实现自然是 runtime.gorecover ,那么我们来一探究竟

runtime.gorecover

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

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

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

  1. 如上可以看到 runtime.gorecover 去对 p.recovered 设置是否恢复
  1. runtime.gopanic 中校验 p.recovered 已处理,则执行 recovery 函数
  1. recovery 函数中去处理对应的寄存器的值去维护上下文
  1. 最后我们可以看到最终调用 gogo 函数跳回原来调用的位置

因此,当我们在同一个协程中出现了 panic,且在同一个协程中去使用 defer 来配合 recover 来进行捕获异常和处理异常,就可以得以实现,看到这里,有没有觉得还是蛮简单的,不就是去对一个 p.recovered 进行配合处理吗

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

总结

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

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

当然,如果文章对你有帮助的话,欢迎留言评论哦

感谢阅读,欢迎交流,点个赞,关注一波 再走吧

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~

文中提到的技术点,感兴趣的可以查看这些文章:

  • GO 中 defer的实现原理
  • GO 中的 defer 有哪些注意事项?上
  • GO 中的 defer 有哪些注意事项?下
  • GO 中的指针?
  • GO 中优雅编码和降低圈复杂度

可以进入地址进行体验和学习:https://xxetb.xet.tech/s/3lucCI

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/129367.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

内网渗透之哈希传递

文章目录 哈希传递(NTLM哈希)概念LMNTLM 原理利用hash传递获取域控RDP 总结 哈希传递(NTLM哈希) 内网渗透中找到域控IP后使用什么攻击手法拿下域控: 扫描域控开放端口。因为域控会开放远程连接:windows开…

常见排序算法Java版(待续)

冒泡排序O(n^2) public class Main {public static void main(String[] args) {Random random new Random();int[] nums new int[]{random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100)};for (i…

SQL Server 简介与 Docker Compose 部署

今天我翻阅了在之前公司工作时的笔记,发现了有关数据库的一些记录。当时,我们的项目开始使用 Oracle 数据库,但后来由于一些项目需求的变更,我们切换到了 SQL Server 。值得一提的是,公司当时也开始采用 Docker 技术&a…

多路彩灯控制器led流水灯VHDL速度可调仿真图视频、源代码

名称:多路彩灯控制器led流水灯VHDL速度可调 软件:Quartus 语言:VHDL 代码功能: 使用VHDL设计彩灯控制器,共24个led灯,分为5种不同的花样,可以通过按键切换花样的变化速度。 代码下载&#…

设计模式 - 行为型模式:策略模式(概述 | 案例实现 | 优缺点 | 使用场景)

目录 一、行为型模式 1.1、策略模式 1.1.1、概论 1.1.2、案例实现 1.1.3、优缺点 1.1.4、使用场景 一、行为型模式 1.1、策略模式 1.1.1、概论 策略模式设计的每一个算法都封装了起来,使他们可以相互替换,通过一个对象委派不同的算法给相应的客户…

vscode ssh linux C++ 程序调试

vscode调试c++程序相比vs2022要复杂很多,vs2022可以"一键运行调试",vscode则需要自己配置。 ​vscode调试程序时,会在当前工作目录产生.vscode 目录, 该目录有两个重要文件launch.json和tasks.json, 下面介绍两种调试方法: 手动调试和自动调试。 手动调试 不管…

用 HTTP 提交数据,基本就这 5 种方式

网页开发中,向服务端提交数据是一个基本功能,工作中会大量用 xhr/fetch 的 api 或者 axios 这种封装了一层的库来做。 可能大家都写过很多 http/https 相关的代码,但是又没有梳理下它们有哪几种呢? 其实通过 http/https 向服务端…

Nosql redis高可用和持久化

Nosql redis高可用和持久化 1、redis高可用2、redis持久化2.1redis持久化2.2Redis 持久化方法2.3RDB 持久化2.3.1RDB持久化工作原理2.3.2触发条件2.3.3其他自动触发机制2.3.4执行流程2.3.5启动时加载 2.4AOF 持久化2.4.1AOF持久化原理2.4.2开启AOF2.4.3执行流程2.4.4文件重写的…

java基础 日期工具类

目录结构: DateUtils.java package dateStudy; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date;public class DateUtils {private static final String FORMAT_1"yyyy-MM-dd HH:mm:ss";//私有方法&#xf…

简易版Pycharm(2023)+Conda开发环境配置教程

困 扰 不知道为什么,自从Pycharm更新了新的版本以后,在Pycharm中为项目工程配置Python解释器环境时,总是不能像以前那么方便。 比如,当前Conda中有十个不同的开发环境,每个环境一个名称,比如,p…

JavaScript Web APIs第五天笔记

Web APIs - 第5天笔记 目标: 能够利用JS操作浏览器,具备利用本地存储实现学生就业表的能力 BOM操作综合案例 js组成 JavaScript的组成 ECMAScript: 规定了js基础语法核心知识。比如:变量、分支语句、循环语句、对象等等 Web APIs : DOM 文档对象模型&…

1. QEMU背景知识

QEMU的两种模拟方案: 系统模拟 System Simulation:可以简单的理解为对整个计算机系统进行模拟(CPU、Memory等) 用户模式模拟 User Mode Simulation:基于模拟方案执行特定应用程序 除了模拟功能外,QUME工具…