defer
defer 语句用于延迟函数的调用,常用于关闭文件描述符、释放锁等资源释放场景。但 defer 关键字只能作用于函数或函数调用。
defer func(){ // 函数
fmt.Print("Hello,World!")
}()
defer fmt.Print("Hello,World!") // 函数调用
1. 执行机制
1.1 执行时机
在Go语言的函数中 return
语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer
语句执行的实际就在返回值操作后,RET指令前。具体如下图所示:
1.2 行为规则
1)规则一:延迟函数的参数在 defer 语句出现时就已经确定了
示例如下:
func a() {
i := 0
defer fmt.Println(i) // 程序运行打印 0
i++
return
}
defer 语句中的 fmt.Println() 参数 i 值 在 defer 出现时就已经确定了,实际上是复制了一份。后面对变量 i 的修改不会影响 fmt.Println() 函数的调用,依旧打印 0。
对于指针类型参数,此规则依然适用,只不过延迟函数的参数是一个地址值,在这种情况下,defer 后面的语句对变量的修改可能会影响延迟函数。
2)规则二:延迟函数按照后进先出
的顺序执行
设计 defer 的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如申请资源的顺序时 A→B→C,释放的顺序往往又要反向进行。这就是把 defer 设计成 LIFO
的原因
3)规则三:延迟函数可能操作主函数的具名返回值
定义 defer 的函数(下称主函数)可能有返回值,返回值可能有名字(具名返回值),也可能没有返回值(匿名返回值),延迟函数可能会影响返回值。
举个栗子:
func deferFuncReturn() (result int){
i := 1
defer func() {
result++
}()
return i // 程序返回 2
}
上面已经介绍过了 defer 的执行时机,该函数的 return 语句可以拆分成下面三行:
result = i
result++
return
主函数有不同的返回方式,包括匿名返回值和具名返回值,但万变不离其宗,只要把 return 语句拆开都可以很好理解,下面分别举例说明:
(1)主函数拥有匿名返回值,返回字面值
一个主函数拥有一个匿名返回值,返回时使用字面值,这种情况下 defer 语句时无法操作返回值的
func foo() int {
var i int
defer func() {
i++
)()
return 1
}
上面的 return 语句直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值
(2)主函数拥有匿名返回值,返回变量
一个主函数拥有一个匿名返回值,返回本地或全局变量,这种情况下 defer 语句可以引用返回值,反不会改变返回值
func foo() int {
var i int
defer func(){
i++
}
return i
}
假定返回值变量为 anony ,上面的返回语句可以拆分为以下过程:
annnoy = i
i++
return
函数返回 0
(3)主函数拥有具名返回值
主函数声明语句中带名字的返回值会被初始化为一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,则可能改变返回结果。
一个影响函数返回值的例子:
func foo() (ret int) {
defer func() {
ret++
}()
return 0
上面的函数拆解出来如下所示:
ret = 0
ret++
return
函数真正返回前,在 defer 中对返回值做了 +1 操作,所以函数最终返回 1
2. 实现原理
2.1 数据结构
源码包中 src/src/runtime/runtime2.go:_defer 定义了 defer 的数据结构
type _defer struct {
...
sp uintptr // 函数栈指针
pc uintptr // 程序计数器
fn func() // 函数地址
link *_defer // 指向自身结构的指针,用于链接多个 defer
...
}
编译器会把每个延迟函数编译成一个 _defer 实例暂存到 goroutine 数据结构中,待函数结束时再逐个取出执行。
每个defer 语句对应一个 _defer 实例,多个实例使用指针 link 链接起来形成一个单链表,保存到 goroutine 数据结构中。
goroutine 的数据结构如下所示:
type g struct {
...
_defer *_defer // defer 链表
...
}
每次插入 _defer 实例时均插入链表头部,函数执行结束时再依次从头部取出,从而实现后进先出的效果。
一个 goroutine 可能连续调用多个函数,defer 的添加过程跟上述流程一致,进入函数时添加 defer ,离开函数时取出 defer ,所以即便调用多个函数,也总是能保证 defer 是按 LIFO 方式执行的。
3. 小结
- defer 定义的延迟函数参数在 defer 语句出现时就已经确定了
- defer 定义的顺序与实际地执行顺序相反
- return 不是原子操作,执行过程是:保存返回值 → 执行 defer → 执行 ret 跳转
- 申请资源后立即使用 defer 关闭资源是一个好习惯