https://mp.weixin.qq.com/s/gaC2gmFhJezH-9-uxpz07w
https://www.bilibili.com/video/BV1hv411x7we?p=9
https://www.bilibili.com/video/BV1hv411x7we?p=10
要点总结
defer的工作原理和倒序执行原因
- 工作原理:
defer
通过deferproc
和deferreturn
两个运行时函数实现。deferproc
在遇到defer
语句时调用,负责保存要执行的函数信息,包括函数参数和返回值的大小,以及函数的入口地址。deferreturn
在函数返回前被调用,执行所有已注册的defer
函数。
- 倒序执行:由于
defer
信息存储在链表中,并且新注册的defer
总是被添加到链表的头部,所以当执行deferreturn
时,会从链表头部开始执行,导致defer
函数按照注册的逆序执行。
defer链表和结构体
- 链表:每个
goroutine
都有一个与之对应的runtime.g
结构体,其中的_defer
字段指向一个链表,用于存储defer
信息。 - 结构体:
_defer
结构体包含多个字段,其中siz
表示参数和返回值的总大小,started
标识函数是否已经开始执行,sp
和pc
分别记录了defer
时的栈指针和下一条要执行的指令地址,fn
是指向被注册函数的指针,_panic
用于处理 panic 时的defer
,link
指向链表中的前一个_defer
结构体。
type _defer struct {
siz int32 // 参数和返回值的总大小
started bool // 标识 defer 函数是否已经开始执行
sp uintptr // 在 defer 时的栈指针
pc uintptr // deferproc 函数返回后要继续执行的指令地址
fn *funcval // 被注册的 defer 函数
_panic *_panic // 触发 defer 函数执行的 panic 指针,正常情况下为 nil
link *_defer // 指向链表中前一个 _defer 结构的指针
}
defer传参机制
- 参数传递:当
defer
一个函数时,其参数在deferproc
调用时被复制到堆上分配的空间中。当defer
函数实际执行时,这些参数再从堆复制到调用者函数的栈上。 - 优化:对于没有捕获列表的函数,编译器会在只读数据段分配一个共用的
funcval
结构体,以优化性能。
defer与闭包
- 闭包形成:当
defer
的函数是一个闭包,即捕获了外层函数的变量时,闭包中的函数值和捕获的变量都会被存储在堆上。 - 变量处理:捕获的变量如果被修改过,会在堆上分配空间,而栈上只存储变量的地址。闭包对象在创建时会包含指向闭包函数入口的指针和捕获变量的地址列表。
defer的性能问题(Go 1.12)
- 性能问题:在 Go 1.12 中,
defer
的性能问题主要源于两个方面:- 堆分配:
_defer
结构体需要在堆上分配,即使有预分配的deferpool
,也需要进行堆上的获取与释放。 - 参数复制:
defer
函数的参数需要在注册时从栈复制到堆,在执行时再从堆复制到栈,增加了额外的开销。
- 堆分配:
Go 1.13 的优化
- 栈分配优化:在 Go 1.13 中,引入了
deferprocStack
函数,它允许将_defer
结构体直接分配在函数的栈帧中,而不是在堆上。这减少了堆分配和回收的开销。
- 限制:尽管 1.13 版本减少了堆分配,但这种优化并不适用于所有情况。特别是循环中的
defer
,包括显式的for
循环和通过goto
形成的隐式循环,仍然需要在堆上分配。
Go 1.14 的优化(open coded defer)
- 内联调用:Go 1.14 进一步优化了
defer
的性能,通过所谓的”open coded defer”,即直接在函数内部展开并调用defer
函数,而不是通过_defer
结构体和链表。 - 条件执行:对于可能不会执行的
defer
函数,Go 1.14 引入了一个标识变量df
,用于跟踪哪些defer
函数需要被执行。这允许编译器在编译时就确定是否需要创建_defer
结构体。 - 性能提升:通过这种方式,Go 1.14 显著提高了
defer
的性能,因为它避免了创建和维护_defer
链表的开销。
性能测试
- 基准测试:文章中提供了一个基准测试函数
BenchmarkDefer
,用于比较不同 Go 版本中defer
的性能。 - 结果:基准测试结果显示,从 Go 1.12 到 Go 1.14,
defer
的性能有了显著的提升。Go 1.14 的”open coded defer”相比于 Go 1.12 的实现,性能提升了近一个数量级。
总结
- 性能提升:Go 语言的
defer
机制在 1.12 版本中存在性能问题,但在 1.13 和 1.14 版本中得到了显著的优化。 - 理解重要性:理解
defer
的注册与执行逻辑对于编写高效的 Go 代码至关重要,尤其是在涉及到性能敏感的应用时。 - panic 处理:尽管
defer
的性能得到了提升,但在处理 panic 时,Go 1.14 版本可能会稍微慢一些,因为需要通过栈扫描来找到并执行那些使用”open coded defer”优化的函数。
面试题
当然可以,以下是整理好的三道面试题及其答案:
https://mp.weixin.qq.com/s/iiOr6IlwDiwMRgXFMWiGxw
面试题 1:defer
的执行顺序
- 问题:在 Go 语言中,
defer
语句的执行顺序是怎样的? - 答案:
defer
语句按照它们在函数中出现的逆序执行,即最后一个声明的defer
会第一个执行,这遵循了后进先出(LIFO)的原则。
面试题 2:defer
编程题
- 问题:以下代码的输出结果是什么?
package main import “fmt”
func main() { a := 1 b := 3 defer func() { CalNum(“1”, a, CalNum(“123”, a, b)) }() a = 2 defer func() { defer CalNum(“1234”, a, CalNum(“12345”, a, b)) }() b = 4 } func CalNum(index string, a, b int) int { ret := a + b fmt.Println(index, a, b, ret) return ret }
- **答案**:代码的输出结果是:
```
12345 2 4 6
1234 2 6 8
1 1 3 4
解释:最内层的`CalNum`("12345")首先执行,打印参数值(2, 4),然后是外层的`CalNum`("1234"),依此类推。注意`a`和`b`的值在`defer`调用之间发生了变化,因为`defer`表达式的参数在`defer`语句执行时就已经确定。
题目
https://mp.weixin.qq.com/s/4sCYf3icR6R-gHBNhGDxuw
题目 1
func f1() (result int) {
defer func() {
result++
}()
return 0
}
题目 2
func f2() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
题目 3
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
答案
第一题:输出结果为 1
第二题:输出结果为 5
第三题:输出结果为 1
看一下你答对了几题,是不是和你心中所想不一样?
接下来详细去分析在 Go 语言中 defer
关键字和 return
关键字的执行顺序以及上面的答案为什么是这样。
defer
defer
是 Go 语言里面提供的关键字,用于声明一个延迟函数,一般用于资源的释放,例如文件资源和网络连接等,标记了 defer
的语句一般在 return
语句之前执行,如果有多个 defer
语句,则遵循栈的调用规则,越后面的语句越先执行。
代码示例:
f, err := os.Open(filename)
if err != nil {
log.Println("open file error: ", err)
}
defer f.Close()
坑
上面简单概括了 defer
的执行顺序和机制,但为什么前面三道题的答案和我们想的都不一样呢?
defer
在 return
之前执行这个肯定的,在官方文档中也有说到,那么可能存在问题的就是 return
语句,这也是最重要的一点, return** 语句不是原子操作!**
这是什么意思呢?就是说 return
语句实际上是分为两步完成的,第一步给返回值赋值,第二步返回值。那么 defer
语句就可能在赋值和返回值之间修改返回值,使最终的函数返回值与你想象的不一致。
可以把 return
拆分来写,就会更加清楚的明白这个原理。
func f1() (result int) {
return = 0
defer ...
return result
}
答案解析
我们可以把三个题目的代码分别拆开来写,就能够明白为什么。
题目 1
func f1() (result int) {
result = 0 // 先给 result 赋值
func() { // defer 插入到赋值和返回之间,修改了 result 的值
result++
}()
return // 最后返回的就是被修改了的值
}
题目 2
func f2() (result int) {
tmp := 5
result = tmp // 赋值
func() { // defer 被插入到赋值与返回之间执行,但是并没有改动到 result
tmp = tmp + 5
}
return // 最后返回的 return 就是 5
}
题目 3
func f3() (result int) {
result = 1 // 给返回值赋值
func(result int) { // 这里改的 result 是传值传进去的 result,不会改变前面赋值的 result
result = result + 5
}(result)
return // 最后 return 的是 1
}
结论
defer
确实是在 return
前面调用的,但是由于 return** 并不是一个原子操作,**defer**
语句的执行是在 **return**
的赋值和返回之间**,所以如果 defer
语句涉及到了修改返回值,那么就会改变最后 return
的值。
文档信息
- 本文作者:Zzhiter
- 本文链接:http://zzhiter.top/2024/05/21/defer-%E5%8E%9F%E7%90%86%E5%8F%8A%E9%9D%A2%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)