Go语言中文网,致力于每日分享编码知识,欢迎关注我,会有意想不到的收获!
![5283795dca73939c5b5b14507ac72030.png](https://i-blog.csdnimg.cn/blog_migrate/504e23ded9a92d72522359695b3640f1.jpeg)
在上一章节 4个问题让你深入理解 Go 的 panic 和 recover 中,我们发现了 defer 与其关联性极大,还是觉得非常有必要深入一下。希望通过本章节大家可以对 defer 关键字有一个深刻的理解,那么我们开始吧。你先等等,请排好队,我们这儿采取后进先出 LIFO 的出站方式...
01 特性
我们简单的过一下 defer 关键字的基础使用,让大家先有一个基础的认知
一、延迟调用
func main() {defer log.Println("EDDYCJY.")log.Println("end.")}
输出结果:
$ go run main.go 2019/05/19 21:15:02 end.2019/05/19 21:15:02 EDDYCJY.
二、后进先出
func main() {for i := 0; i < 6; i++ {defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".")}log.Println("end.")}
输出结果:
$ go run main.go2019/05/19 21:19:17 end.2019/05/19 21:19:17 EDDYCJY5.2019/05/19 21:19:17 EDDYCJY4.2019/05/19 21:19:17 EDDYCJY3.2019/05/19 21:19:17 EDDYCJY2.2019/05/19 21:19:17 EDDYCJY1.2019/05/19 21:19:17 EDDYCJY0.
三、运行时间点
func main() {func() { defer log.Println("defer.EDDYCJY.")}()log.Println("main.EDDYCJY.")}
输出结果:
$ go run main.go 2019/05/22 23:30:27 defer.EDDYCJY.2019/05/22 23:30:27 main.EDDYCJY.
四、异常处理
func main() {defer func() {if e := recover(); e != nil {log.Println("EDDYCJY.")}}()panic("end.")}
输出结果:
$ go run main.go 2019/05/20 22:22:57 EDDYCJY.
![76a61c4fd97e12b1ea5d092e1b96c17e.png](https://i-blog.csdnimg.cn/blog_migrate/d5cc54964177e3d00a79d3c3473ac8ef.jpeg)
02 源码剖析
![ab3bba4fb35dfa07c4874ed402cdd768.png](https://i-blog.csdnimg.cn/blog_migrate/0189a20fb8f1c3599a4b769fc4be7792.jpeg)
首先我们需要找到它,找到它实际对应什么执行代码。通过汇编代码,可得知涉及如下方法:
- runtime.deferproc
- runtime.deferreturn
很显然是运行时的方法,是对的人。我们继续往下走看看都分别承担了什么行为
数据结构
在开始前我们需要先介绍一下 defer 的基础单元 _defer 结构体,如下:
![7ad1f19ad4b98ec11972a75c0e014437.png](https://i-blog.csdnimg.cn/blog_migrate/12e63033f8ef80b496bdd7fc53f7ab0e.jpeg)
- siz:所有传入参数的总大小
- started:该 defer 是否已经执行过
- sp:函数栈指针寄存器,一般指向当前函数栈的栈顶
- pc:程序计数器,有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC指向的是下一条指令,而不是当前指令
- fn:指向传入的函数地址和参数
- _panic:指向 _panic 链表
- link:指向 _defer 链表
![9f0ad9d0f4cecdfb265cb56c25d733bb.png](https://i-blog.csdnimg.cn/blog_migrate/29004ec148a71180be3e369de6ab2046.jpeg)
deferproc
![5cbcdba97c9764dd892c0225e1d9f33c.png](https://i-blog.csdnimg.cn/blog_migrate/173dfffa6836bc9229b7e5107e8707f2.jpeg)
- 获取调用 defer 函数的函数栈指针、传入函数的参数具体地址以及PC (程序计数器),也就是下一个要执行的指令。这些相当于是预备参数,便于后续的流转控制
- 创建一个新的 defer 最小单元 _defer,填入先前准备的参数
- 调用 memmove 将传入的参数存储到新 _defer (当前使用)中去,便于后续的使用
- 最后调用 return0 进行返回,这个函数非常重要。能够避免在 deferproc 中又因为返回 return,而诱发 deferreturn 方法的调用。其根本原因是一个停止 panic 的延迟方法会使 deferproc 返回 1,但在机制中如果 deferproc 返回不等于 0,将会总是检查返回值并跳转到函数的末尾。而 return0 返回的就是 0,因此可以防止重复调用
小结
在这个函数中会为新的 _defer 设置一些基础属性,并将调用函数的参数集传入。最后通过特殊的返回方法结束函数调用。另外这一块与先前 《深入理解 Go panic and recover》 的处理逻辑有一定关联性,其实就是 gp.sched.ret 返回 0 还是 1 会分流至不同处理方式
newdefer
![8be5e4df4599bbf82467841832021e71.png](https://i-blog.csdnimg.cn/blog_migrate/233a715f770f561e33c53209ad16019c.jpeg)
- 从池中获取可以使用的 _defer,则复用作为新的基础单元
- 若在池中没有获取到可用的,则调用 mallocgc 重新申请一个新的
- 设置 defer 的基础属性,最后修改当前 Goroutine 的 _defer 指向
通过这个方法我们可以注意到两点,如下:
- defer 与 Goroutine(g) 有直接关系,所以讨论 defer 时基本离不开 g 的关联
- 新的 defer 总是会在现有的链表中的最前面,也就是 defer 的特性后进先出
小结
这个函数主要承担了获取新的 _defer 的作用,它有可能是从 deferpool 中获取的,也有可能是重新申请的
deferreturn
![2f85f68d1bd48d3bbc759322bade3e14.png](https://i-blog.csdnimg.cn/blog_migrate/10cebb485519f3205a6a354412eb51e1.jpeg)
如果在一个方法中调用过 defer 关键字,那么编译器将会在结尾处插入 deferreturn 方法的调用。而该方法中主要做了如下事项:
- 清空当前节点 _defer 被调用的函数调用信息
- 释放当前节点的 _defer 的存储信息并放回池中(便于复用)
- 跳转到调用 defer 关键字的调用函数处
在这段代码中,跳转方法 jmpdefer 格外重要。因为它显式的控制了流转,代码如下:
![1707c83486eb506130d8c1547633cbb0.png](https://i-blog.csdnimg.cn/blog_migrate/484e82e05dd47c79f870f3fa781617bd.jpeg)
通过源码的分析,我们发现它做了两个很 “奇怪” 又很重要的事,如下:
- MOVQ-8(SP), BP:-8(BX) 这个位置保存的是 deferreturn 执行完毕后的地址
- SUBQ$5, (SP):SP 的地址减 5 ,其减掉的长度就恰好是 runtime.deferreturn 的长度
你可能会问,为什么是 5?好吧。翻了半天最后看了一下汇编代码...嗯,相减的确是 5 没毛病,如下:
0x007a 00122 (main.go:7)CALLruntime.deferreturn(SB)0x007f 00127 (main.go:7)MOVQ56(SP), BP
我们整理一下思绪,照上述逻辑的话,那 deferreturn 就是一个 “递归” 了哦。每次都会重新回到 deferreturn 函数,那它在什么时候才会结束呢,如下:
func deferreturn(arg0 uintptr) {gp := getg()d := gp._deferif d == nil {return}...}
也就是会不断地进入 deferreturn 函数,判断链表中是否还存着 _defer。若已经不存在了,则返回,结束掉它。简单来讲,就是处理完全部 defer 才允许你真的离开它。果真如此吗?我们再看看上面的汇编代码,如下:
![568d044c2bb7527b4581e691c68c3c33.png](https://i-blog.csdnimg.cn/blog_migrate/512972e1410090bd3a32b23f2a765510.jpeg)
的确如上述流程所分析一致,验证完毕
小结
这个函数主要承担了清空已使用的 defer 和跳转到调用 defer 关键字的函数处,非常重要
03 总结
![69d4ae696e1163d935f66fdfb1291ecc.png](https://i-blog.csdnimg.cn/blog_migrate/39c3ce36a7a0d220672955e6410c3eee.jpeg)
我们有提到 defer 关键字涉及两个核心的函数,分别是 deferproc 和 deferreturn 函数。而 deferreturn 函数比较特殊,是当应用函数调用 defer 关键字时,编译器会在其结尾处插入 deferreturn 的调用,它们俩一般都是成对出现的
但是当一个 Goroutine 上存在着多次 defer 行为(也就是多个 _defer)时,编译器会进行利用一些小技巧, 重新回到 deferreturn 函数去消耗 _defer 链表,直到一个不剩才允许真正的结束
而新增的基础单元 _defer,有可能是被复用的,也有可能是全新申请的。它最后都会被追加到 _defer 链表的表头,从而设定了后进先出的调用特性
关联
- 4个问题让你深入理解 Go 的 panic 和 recover
参考
- 「GCTT 出品」Golang - 操作系统调度器玩法「第一部分」
- Dive into stack and defer/panic/recover in go
- golang-notes
本文作者:煎鱼,原创投稿发布