热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

go如何将int设成nil_大神是如何学习Go语言之panic和recover的原理

概述在具体介绍和分析Go语言中的panic和recover的实现原理之前,我们首先需要对它们有一些基本的了解;panic和recover两个关键字其实都

概述

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

a6fd2db4cc35beb272ae5d36b0b211cd.png

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

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

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

常见使用

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

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 函数,这一点从 上一节 的源代码中也有所体现。

另一个例子就不止涉及 panic 和 defer 关键字了,我们可以看一下 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 函数也能够正常执行

实现原理

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

panic 和 recover 关键字会在 编译期间 被 Go 语言的编译器转换成 OPANIC 和 ORECOVER 类型的节点并进一步转换成 gopanic 和 gorecover 两个运行时的函数调用。

数据结构

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 的关联形成一个链表。

崩溃

首先了解一下没有被 recover 的 panic 函数是如何终止整个程序的,我们来看一下 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 系统调用来退出程序。

恢复

到了这里我们已经掌握了 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 参数。

总结

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

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

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

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

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

    2. recovery 会根据传入的 pc 和 sp 跳转到 deferproc 函数;

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

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

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

Reference

  • Dive into stack and defer/panic/recover in go

  • Defer, Panic, and Recover

推荐阅读

  • 大神是如何学习 Go 语言之浅谈 select 的实现原理

  • 大神是如何学习 Go 语言之 Channel 实现原理精要


喜欢本文的朋友,欢迎关注“Go语言中文网”:

32942f7edb93bb91a4f85b9924142976.png

Go语言中文网启用微信学习交流群,欢迎加微信:274768166




推荐阅读
  • java drools5_Java Drools5.1 规则流基础【示例】(中)
    五、规则文件及规则流EduInfoRule.drl:packagemyrules;importsample.Employ;ruleBachelorruleflow-group ... [详细]
  • tcpdump 4.5.1 crash 深入分析
    tcpdump 4.5.1 crash 深入分析 ... [详细]
  • 湍流|低频_youcans 的 OpenCV 例程 200 篇106. 退化图像的逆滤波
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了youcans的OpenCV例程200篇106.退化图像的逆滤波相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • switch语句的一些用法及注意事项
    本文介绍了使用switch语句时的一些用法和注意事项,包括如何实现"fall through"、default语句的作用、在case语句中定义变量时可能出现的问题以及解决方法。同时也提到了C#严格控制switch分支不允许贯穿的规定。通过本文的介绍,读者可以更好地理解和使用switch语句。 ... [详细]
  • 加密世界下一个主流叙事领域:L2、跨链桥、GameFi等
    本文介绍了加密世界下一个主流叙事的七个潜力领域,包括L2、跨链桥、GameFi等。L2作为以太坊的二层解决方案,在过去一年取得了巨大成功,跨链桥和互操作性是多链Web3中最重要的因素。去中心化的数据存储领域也具有巨大潜力,未来云存储市场有望达到1500亿美元。DAO和社交代币将成为购买和控制现实世界资产的重要方式,而GameFi作为数字资产在高收入游戏中的应用有望推动数字资产走向主流。衍生品市场也在不断发展壮大。 ... [详细]
  • 3.223.28周学习总结中的贪心作业收获及困惑
    本文是对3.223.28周学习总结中的贪心作业进行总结,作者在解题过程中参考了他人的代码,但前提是要先理解题目并有解题思路。作者分享了自己在贪心作业中的收获,同时提到了一道让他困惑的题目,即input details部分引发的疑惑。 ... [详细]
  • 颜色迁移(reinhard VS welsh)
    不要谈什么天分,运气,你需要的是一个截稿日,以及一个不交稿就能打爆你狗头的人,然后你就会被自己的才华吓到。------ ... [详细]
  • 正则表达式及其范例
    为什么80%的码农都做不了架构师?一、前言部分控制台输入的字符串,编译成java字符串之后才送进内存,比如控制台打\, ... [详细]
  • 精讲代理设计模式
    代理设计模式为其他对象提供一种代理以控制对这个对象的访问。代理模式实现原理代理模式主要包含三个角色,即抽象主题角色(Subject)、委托类角色(被代理角色ÿ ... [详细]
  • 后台自动化测试与持续部署实践
    后台自动化测试与持续部署实践https:mp.weixin.qq.comslqwGUCKZM0AvEw_xh-7BDA后台自动化测试与持续部署实践原创 腾讯程序员 腾讯技术工程 2 ... [详细]
  • 基于词向量计算文本相似度1.测试数据:链接:https:pan.baidu.coms1fXJjcujAmAwTfsuTg2CbWA提取码:f4vx2.实验代码:imp ... [详细]
  • 开发笔记:线性回归读取txt
    txt中部分数据如下:1.0000000.067732 ... [详细]
author-avatar
mobiledu2502918487
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有