defer应用
Go 语言的 defer
会在当前函数或者方法返回之前执行传入的函数。它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。
比如解锁资源:
mu.Lock()
defer mu.Unlock()
我们在 Go 语言中使用 defer 时会遇到两个比较常见的问题,这里会介绍具体的场景并分析这两个现象背后的设计原理:
- defer 关键字的调用时机以及多次调用 defer 时执行顺序是如何确定的;
- defer 关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果;
defer触发时机
defer的触发时机主要有三个:
- 函数执行到函数体末端
- 函数执行return语句
- 当前协程panic
defer执行顺序
直接用go程序演示:
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
其输出为:
4 3 2 1 0
运行上述代码会倒序执行所有向 defer 关键字中传入的表达式,最后一次 defer 调用传入了 fmt.Println(4)
,所以会这段代码会优先打印 4。
我们可以通过下面这个简单例子强化对 defer 执行时机的理解:
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 传入的函数不是在退出代码块的作用域时执行的,它会在当前函数和方法返回之前被调用。
预计算参数
假设我们想要计算 main 函数运行的时间,可能会写出以下的代码:
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
$ go run main.go
0s
我们理想的输出结果应该是1s
,但是上述代码的运行结果并不符合我们的预期。
调用 defer 关键字会立刻对函数中引用的外部参数进行拷贝,所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出 0s。
想要解决这个问题的方法非常简单,我们只需要向 defer 关键字传入匿名函数:
func main() {
startedAt := time.Now()
defer func() { fmt.Println(time.Since(startedAt)) }()
time.Sleep(time.Second)
}
$ go run main.go
1s
虽然调用 defer 关键字时也使用值传递,但是因为拷贝的是函数指针,所以 time.Since(startedAt)
会在 main 函数返回前被调用并打印出符合预期的结果。
defer实现原理
首先来了解一下 defer 关键字在 Go 语言源代码中对应的数据结构,defer数据结构的源码在src/runtime/runtime2.go
中定义:
type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
我们简单介绍一下 runtime._defer 结构体中的几个字段:
-
siz
是参数和结果的内存大小;
-
sp
和 pc
分别代表栈指针和调用方的程序计数器;
-
fn
是 defer 关键字中传入的函数;
-
_panic
是触发延迟调用的结构体,可能为空;
-
link
:注意到:*_defer
,说明了这个数据结构实际上是一个链表。
除了上述的这些字段之外,runtime._defer
中还包含一些垃圾回收机制使用的字段,这里为了减少理解的成本就都省去了。
中间代码生成阶段执行的被 cmd/compile/internal/gc.state.stmt
函数会处理 defer 关键字。从下面截取的这段代码中,我们会发现编译器调用了 cmd/compile/internal/gc.state.call
函数,这表示 defer 在编译器看来也是函数调用:
func (s *state) stmt(n *Node) {
switch n.Op {
case ODEFER:
s.call(n.Left, callDefer)
}
}
对于defer关键字,主要有3个函数:
-
deferproc
。在每遇到一个defer关键字时,实际上都会转换为deferproc函数,deferproc函数的作用是将defer函数存入链表中。
-
deferreturn
。在return指令前调用,从链表中取出defer函数并执行。
-
deferprocStack
。go1.13后对defer做的优化,通过利用栈空间提高效率。
- 编译期;
- 将 defer 关键字被转换
runtime.deferproc
;
- 在调用 defer 关键字的函数返回之前插入
runtime.deferreturn
;
- 运行时:
-
runtime.deferproc
会将一个新的 runtime._defer
结构体追加到当前 Goroutine 的链表头;
-
runtime.deferreturn
会从 Goroutine 的链表中取出 runtime._defer
结构并依次执行;
如果要了解详情,参考:理解Go语言defer关键字的原理