现象
作用域
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
$ go run main.go
4
3
2
1
0
运行上述代码会倒序执行传入 defer 关键字的所有表达式,因为最后一次调用 defer 时传入了 fmt.Println(4),所以这段代码会优先打印 4。
func main() {
{
defer fmt.Println("defer runs")
fmt.Println("block ends")
}
fmt.Println("main ends")
}
$ go run main.go
block ends
main ends
defer runs
defer 传入的函数不是在退出代码块的作用域时执行的,它只会在当前函数和方法返回之前被调用。
预计算参数
Go 语言中所有的函数调用都是传值的,虽然 defer 是关键字,但是也继承了这个特性。
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
$ go run main.go
164ns
调用 defer 关键字会立刻拷贝函数中引用的外部参数,所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出不为 1s。
func main() {
startedAt := time.Now()
defer func() { fmt.Println(time.Since(startedAt)) }()
time.Sleep(time.Second)
}
$ go run main.go
1.003950198s
虽然调用 defer 关键字时也使用值传递,但是因为拷贝的是函数指针,所以 time.Since(startedAt) 会在 main 函数返回前调用并打印出符合预期的结果。
数据结构
在介绍 defer 函数的执行过程与实现原理之前,首先来了解一下 defer 关键字在 Go 语言源代码中对应的数据结构。
type _defer struct {
siz int32
started bool
openDefer bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
runtime._defer 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。
runtime._defer 结构体中的几个字段:
siz是参数和结果的内存大小;sp和pc分别代表栈指针和调用方的程序计数器;fn是defer关键字中传入的函数;_panic是触发延迟调用的结构体,可能为空;openDefer表示当前defer是否经过开放编码的优化; 除了上述的这些字段之外,runtime._defer中还包含一些垃圾回收机制使用的字段,这里为了减少理解的成本就都省去了。
执行机制
中间代码生成阶段的 cmd/compile/internal/gc.state.stmt 会负责处理程序中的 defer,该函数会根据条件的不同,使用三种不同的机制处理该关键字。
func (s *state) stmt(n *Node) {
...
switch n.Op {
case ODEFER:
if s.hasOpenDefers {
s.openDeferRecord(n.Left) // 开放编码
} else {
d := callDefer // 堆分配
if n.Esc == EscNever {
d = callDeferStack // 栈分配
}
s.callResult(n.Left, d)
}
}
}
堆分配、栈分配和开放编码是处理 defer 关键字的三种方法,早期的 Go 语言会在堆上分配 runtime._defer 结构体,不过该实现的性能较差,Go 语言在 1.13 中引入栈上分配的结构体,减少了 30% 的额外开销,并在 1.14 中引入了基于开放编码的 defer,使得该关键字的额外开销可以忽略不计。
小结
defer 关键字的实现主要依靠编译器和运行时的协作:
- 堆上分配 · 1.1 ~ 1.12
- 编译期将
defer关键字转换成runtime.deferproc并在调用defer关键字的函数返回之前插入runtime.deferreturn; - 运行时调用
runtime.deferproc会将一个新的runtime._defer结构体追加到当前 Goroutine 的链表头; - 运行时调用
runtime.deferreturn会从 Goroutine 的链表中取出runtime._defer结构并依次执行;
- 编译期将
- 栈上分配 · 1.13
- 当该关键字在函数体中最多执行一次时,编译期间的
cmd/compile/internal/gc.state.call会将结构体分配到栈上并调用runtime.deferprocStack;
- 当该关键字在函数体中最多执行一次时,编译期间的
- 开放编码 · 1.14 ~ 现在
- 编译期间判断
defer关键字、return语句的个数确定是否开启开放编码优化; - 通过
deferBits和cmd/compile/internal/gc.openDeferInfo存储defer关键字的相关信息; - 如果
defer关键字的执行可以在编译期间确定,会在函数返回前直接插入相应的代码,否则会由运行时的runtime.deferreturn处理; 在本节前面提到的两个现象在这里也可以解释清楚了:
- 编译期间判断
- 后调用的
defer函数会先执行:- 后调用的
defer函数会被追加到 Goroutine_defer链表的最前面; - 运行
runtime._defer时是从前到后依次执行;
- 后调用的
- 函数的参数会被预先计算;
- 调用
runtime.deferproc函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算;
- 调用