defer 估计是每个 Gopher 每天写代码都会写,那么你是不是真正的理解了 defer 呢?不妨看一下下面这个代码片段,这个是我之前给 UC 那边一个 team 做 Golang 培训的时候想的例子。
1. return 语句
在解析上面的题目之前,要理解一个前提是 Go 的函数返回值是通过堆栈返回的,这也是实现了多返回值的方法。举个例子。
查看汇编代码如下。
也就是说 return 语句不是原子操作,而是被拆成了两步
而 defer 语句就是在这两条语句之间执行,也就是
另外在 Go 语言的 func 声明中如果返回值变量显示声明,也就是 func foo() (ret int) {}
的时候,rval 就是 ret。这么上面的题目中对于的函数执行简单来说就是如下代码片段。但是 f3 涉及到另外一个知识点,也就是闭包。
2. 闭包
简单来说,Go 语言中的闭包就是在函数内引用函数体之外的数据,这样就会产生一种结果,虽然数据定义是在函数外,但是在函数内部操作数据也会对数据产生影响。如下面的例子所示,foo() 中的匿名函数对 i 的调用就是闭包引用,i++ 会影响外面定义的 i 的值。而 bar() 中的匿名函数是变量拷贝,i++ 并不会修改外部 i 值。这么看的话,开始的 f3() 的输出你是不是知道是多少了呢?
3. defer 的使用场景
在我最开始学习 Go 语言的时候,我看到 defer 的第一反应就是 Python 中的如下语句。也就是说不用显示地关闭文件句柄,除此之外还有网络连接等各种资源都可以放到 defer 里面来释放。
但是随着写代码越来越多,我觉得上面说的这些场景如果明确知道什么时候要释放资源,那么都不是非使用 defer 不可的,因为使用 defer 还是有很大开销的,下面说。使用 defer 的最合适的场景我觉得应该是和 recover 结合使用,也就是说在你不知道的程序何时可能会 panic 的时候,才引入 defer + recover。
4. defer 的底层实现
defer 的底层实现主要由两个函数:
- func deferproc(siz int32, fn *funcval)
- func deferreturn(arg0 uintptr)
看代码。下面的代码执行了两次 defer ,defer 的执行是按 FILO 的次序执行的,也就是说下面代码的输出是
这个就不细说了。看汇编代码。
编译,objdump。
结合代码看,代码中使用了两次 defer,调用了 deferproc 和 deferreturn ,都是匹配成对调用的。我们看一下 Golang 源码里面对 deferproc 和 deferreturn 的实现。
光看 deferproc 的代码只能看到一个申请 defer 对象的过程,并没有看到这个 defer 对象存储在哪里?那么不妨大胆设想一下,defer 对象是以链表的形式关联到 goroutine 上的。我们看一下 deferproc 中调用的 newdefer 函数。
重点看第 44,45 行,gp 是当前的 goroutine,有一个字段 _defer 是用来存放 defer 结构的,然后我们发现 defer 结构有一个 link 字段其实就相当于链表指针。如果熟悉链表操作的话,第 44,45 行结合起来看就是将新的 defer 对象插入到 goroutine 关联的 defer 链表的头部。那么执行的时候就从头执行 defer 就是 FILO 的顺序了,deferreturn 的源码大家自己去看吧。
5. benchmark
看了第 4 部分,我们应该知道 defer 的调用开销相比直接的函数调用确实多了不少,那么有没有 benchmark 来直观的看一下呢?有的。这里使用雨痕的 《Go 语言学习笔记》的 benchmark 程序。
测试结果如下,看的出来差距还是挺大的。
6. 参考
- https://github.com/golang/go/blob/release-branch.go1.12/src/runtime/panic.go#L229
- https://github.com/golang/go/blob/master/src/runtime/asm_amd64.s#L550
- 《Go 语言学习笔记》
最后,我之前只在博客 http://www.legendtkl.com 和知乎上(知乎专栏:Golang Inside)上面写文章,现在开始在公众号(公众号:legendtkl)上面尝试一下,如果你觉得不错,或者之前看过,欢迎关注或者推荐给身边的人。谢谢。