defer
用于延迟函数的调用,常用于关闭文件或者关闭锁的场景。
defer语句采用类似栈的方式,每遇到一个defer就会把defer后面的函数压入栈中,在函数返回前再把栈中的函数依次取出执行。
一般函数正常返回时会执行被defer延迟的函数,特别的遇到return和panic时也会触发延迟函数。
defer作用于资源释放(关闭文件句柄、数据库连接、停止定时器ticker以及关闭管道)、流程控制(控制函数执行顺序,如wait.Group)和异常处理(recover()),但是defer关键字只能作用于函数或者函数调用。
三条defer的行为规则:
- 延迟函数的参数在defer语句出现时就已经确定了
- 延迟函数按后进先出LIFO的顺序执行,即先出现的defer最后执行
- 难点 延迟函数可能操作住函数的具体变量名称返回值,4种情况依次讲解:
(1). 函数返回过程
func deferFuncReturn() (res int) {
i := 1
defer func() {
res++
}()
return i
}
需要知道的是return不是一个原子操作,其分为两步执行,先将i放入栈中作为返回值,然后再进行跳转,而defer的执行正好是在跳转之前,所以defer执行时是有机会操作返回值的。
return语句可以翻译成
res = i
return
defer是在return之前,所以就有如下变化
res = 1
res++
return
所以结果就是i++
(2). 函数有匿名返回值,返回字面量, 这种情况defer是无法操作返回值的。
func foo() int{
var i int
defer func(){
i++
}()
return 1
}
这里是直接返回1
(3). 主函数有匿名返回值,返回变量, 这种情况下defer语句可以引用返回值,但是不会改变返回值
func foo() int {
var i int
defer func(){
i++
}()
return i
}
i在defer之前就会赋值给返回值变量,defer会修改i,但不会修改返回值变量。
(4). 主函数有具体的返回值变量,主函数声明语句中带名字的返回值会被初始化一个局部变量,函数内部可以像使用局部变量一样使用该返回值,如果defer操作该返回值,有可能会改变返回结果。
func foo() (ret int){
defer func(){
ret++
}
return 0
}
这里就好理解了,返回1
ret = 0
ret++
return
下面来看defer的数据结构
type _defer struct{
sp uintptr // 函数栈指针
pc uintptr // 程序计数器
fn *funcval // 函数地址
link *_defer // 用于链接多个defer
}
每个_defer实例是对一个函数的封装,编译器会把每一个延迟函数编译成一个_defer实例暂存到goroutine数据结构中,待函数结束时再逐个取出执行。
多个_defer实例使用指针link链接成一个单链表,保存到goroutine中,下面是goroutine结构中关于_defer的部分
type g struct {
_defer *_defer // defer链表
}
每次插入_defer实例都是从链表的头部插入,函数执行结束再依次从头部取出defer执行。
defer的创建和执行
创建defer: deferproc() 将defer函数处理成_defer实例,并加入goroutine的链表中;
执行defer: deferreturn() 将defer从goroutine中取出并执行。
整个流程就是:编译器在编译阶段把defer语句替换成函数deferproc(),在return前插入函数deferreturn(), 每次执行deferproc()都会创建一个运行时_defer实例并存储,函数返回前执行deferreturn()依次拿出_defer实例并执行。
最后再简述一下堆defer、栈defer和开放编码类型的defer
堆defer主要的问题在于频繁的对内存分配及释放,导致性能较差。
栈defer由于栈空间有限,不能把所有的defer都放在栈中。
开放编码类型的defer 编译器将defer语句直接翻译成相应的执行代码插入到函数的尾部,从而节省了_defer节点转储的代价,这样就使得延迟函数和普通函数一样调用即可,只需要关注执行。
当然也是有限制条件的:
- 编译时禁止使用编译器优化;
- defer出现在循环语句中;
- 单个函数中defer出现8个以上,或者return个数和defer的个数乘积超过15。