9 defer 延迟调用
[TOC]
defer 延迟调用
defer是Go的重要特性之一,defer 语法为【defer 要延迟执行的函数或方法】
defer一般用于资源的释放和panic处理
defer将函数推迟到其所在的函数最后执行(这有个陷阱,defer与return的返回值陷阱)。
func main() {
defer println("defer 打印")
println("main函数")
//main函数
//defer 打印
}使用defer释放资源
defer作为Go的特性之一,defer为Go的编码方式带来很大的变化。
defer释放资源包括文件流、锁、通道等。
文件复制案例:
文件复制需要读文件流和写文件流,读写文件都是占用系统资源的,当文件读取完成应关闭读文件流,使用defer可以很好做到这点。
defer特性
延迟执行
延迟执行是defer的特性,在恰当的地方使用这个特性可以达到不错的效果,如用这个特性做函数运行耗时计算。
参数预计算
defer注册函数或方法时就会进行值拷贝,因此延迟调用的参数会预先固定,而不会等到函数执行完成后再把参数传递到defer注册的函数、方法中。
如果需要defer执行函数或方法时拿到的是最终的值可以进行指针传递。
defer注册时的形参值拷贝案例:
defer1 是值拷贝,无论后面值发生了任何改变都与defer1无关。
defer2 是传递指针,可以拿到最新值亦可修改这个值。
defer多次执行与后进先出(LIFO)执行顺序
defer的执行顺序是后进先出,这意味着【文件复制案例】中是先关闭写文件资源再关闭读文件资源,【参数预计算- defer注册时的形参值拷贝案例】的执行顺序就说明了这一点。
defer与return的返回值陷阱
在defer和return一起使用时会出现一个返回值陷阱。
疑似 defer 在 return 之后执行的案例:
由于defer是在return之后执行,所以main函数中fu()的返回值r变量为100,defer随后才执行Num = 999。
疑似 defer 在 return之前执行的案例:
defer与return的返回值陷阱解释
两个案例执行结果是截然相反的,原因在于return并不是一个原子性操作,它包含了:将返回值保存在栈上、执行defer函数、函数返回;三个步骤。
疑似defer在return之后执行的案例解释为:全局Num = 100;返回值int = 全局Num;全局Num = 999;return;
疑似defer在return之前执行的案例解释为:返回值 r = 100;r = 20;r = 999;return;至始至终改变的都是返回值r。
defer的演进
Go 1.13之前
defer分配在堆区,尽管有全局的缓存池分配,仍然有较大的性能问题,原因在于使用defer不仅涉及堆内存的分配,在一开始还需要存储defer函数中的参数,最后还需要将堆区数据转移到栈中执行,涉及内存的复制。因此defer比函数直接调用要慢很多。
Go 1.13
为了将调用defer函数的成本降到与直接调用函数相同,Go 1.13在大部分情况下将defer语句放置在栈中,避免在堆区分配、复制对象。但是它和之前版本一样,需要将整个defer语句放置到一条链表中,从而能够在函数退出时以LIFO的顺序执行。
将defer添加到链表中被认为是必不可少的,原因在于defer的数量可能是无限的,也可能是动态调用的,例如if判断条件通过才注册defer语言,只有在运行时才能确定defer的个数。
1.13 中包括两种策略,对于最多只调用一个defer的语句使用了栈分配策略,而对于在控制语句中的defer语句依然使用堆分配策略。在大部分情况下defer的使用场景都是比较简单的,这一改变大幅度提高了defer的效率。
Go 1.14
在1.14以后根据不同的场景defer有了3种实现方式。1.14对最多只调用一次的defer再次进行优化,通过编译时实现内联优化。
Go 1.14 defer内联优化
Go 1.13中defer的栈策略进行了比较大的优化,但与直接调用函数还是有很大差距。
更进一步优化是在编译时就确定函数结束直接调用defer函数,以省去将defer的函数放置进defer链表和遍历defer链表的时间。采用这种方式的困难在于有的defer是在if等判断条件符合时才执行defer,这样的defer是需要在运行时才能确定是否要执行。Go编译器通过在栈中初始化1字节的临时变量标记位以位图形式来判断defer的函数是否需要执行。
在函数即将退出时,从后向前遍历临时变量标记位,当前位为1则执行defer对应的函数,当前位为0则不执行任何操作。这样以1个字节的临时变量标记位的代价满足了大部分情况下的需求。
直接调用函数与defer调用的性能测试案例:
100万次的调用耗时差距并不大,在Go 1.14以上版本defer与直接调用的效率差距微乎其微,可以放心使用defer。
Last updated