前言:
最近在看《effetive go》看到defer,由于我平时没怎么用过defer,之前学得又给忘了,看到一道题试着自己推导一下,发现推导错了,所以重新好好再总结一下。作者属于菜鸡级别,所以本文还不会涉及到原理层面,文章的题目也是浅谈。
1. 需求分析
对于某些需要释放资源的函数,引入defer是必要的。比如打开文件,对这个文件进行读写,在函数的最后对文件在进行关闭,释放资源。但是函数一长,程序员就容易忘记在函数最后对资源进行释放,便会为程序埋下雷。那么就引入一种延迟机制,使得一些操作可以在一开始就被定义,但是可以等到函数末尾再去执行。Go实现的这种机制就是defer。
2. 特性
特性将会分为四个部分:延迟特性,后进先出,作用域以及错误处理四个方面来讲。通过这四个特性可以了解对于defer到底该如何使用。
2.1. 延迟特性
在需求分析中也说得很清楚,被defer的函数会等到函数的最后运行。
func test() {
fmt.Println("hello in test")
}
func main() {
defer test()
fmt.Println("hello in main")
}
结果:
hello in main
hello in test
2.2. 后进先出
一个函数中可能会出现多个defer,那么对于多个defer,采取的是后进先出的策略,也就是按照被defer的顺序,从后往前执行,最后defer的函数最先运行。(**提醒:**这并不意味着defer是用栈实现的,实际上其实现很复杂,有堆,栈以及开放源码)
func main() {
for i:=0; i<5; i++ {
defer test(strconv.Itoa(i))
}
fmt.Println("hello in main")
}
结果如下:
hello in main
hello in test4
hello in test3
hello in test2
hello in test1
hello in test0
2.3. 错误处理
用于处理panic,使得panic之后程序能够继续进行。
先看一下代码:
func A() {
fmt.Println("A")
}
func B() {
fmt.Println("B")
panic("panic in B")
}
func C() {
fmt.Println("C")
}
func main() {
A()
B()
C()
}
上述代码执行结果:
A
B
panic: panic in B
可以看到函数C并没有被执行。
但是,如果我们可以预知这种错误,并且能够在运行过程中处理这种错误,使其不会影响程序的运行,然后保证整体程序的运行,那么可以使用defer以及rcover配合来恢复程序的。
这也说明defer不受错误的影响,只要在引发错误的地方之前声明了defer函数,那么该函数即使在后面还出现错误的情况下依然会继续运行。记住一定要在panic之前声明。
对于上面的B函数,可以修改为:
func B() {
fmt.Println("B")
defer func() {
fmt.Println("Func in B")
}()
panic("panic in B")
}
修改后的执行结果:
A
B
Func in B
panic: panic in B
2.4. 作用域
defer的作用域一般只在一个函数体之内
func main() {
func() {
defer fmt.Println("in subFunc")
}()
fmt.Println("in main")
}
结果:
in subFunc
in main
3. 被defer函数的参数和变量问题
先上结论,然后在一个个分析:
- 参数是函数的返回值,那么会优先执行该参数函数
- 闭包中的变量。如果是在主体函数中的局部变量,那么一定要小心使用。因为在执行在被defer的函数中,使用外部变量是使用外部变量的最后一个状态。
- 如果被defer的函数放回的是一个函数,那么实际上被defer的函数是被返回的那个。
3.1. 参数是函数的返回值
如果被defer函数使用参数是函数的返回值,那么会优先执行该参数函数。
手动写下下面代码的执行结果,看看自己能不能写对:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
结果:
entering: b
in b
entering: a
in a
leaving: a
leaving: b
简单说下:main
调用了b
函数。而b
首先defer了一个un
函数,这个un
函数的参数是一个trace
函数,所以即便这个un
函数会在b函数最后才调用,也会优先执行trace
函数。
3.2. 形成闭包的defer函数
这个也是个大坑,需要多加小心。其主要原理就是形成闭包的defer函数,在被调用的时候使用的外部变量是该变量的最后状态。
将上面2.2
节中的代码改写为下面形式:
func main() {
for i:=0; i<5; i++ {
defer func() {
fmt.Println("hello in test" + strconv.Itoa(i))
}()
}
fmt.Println("in main")
}
结果如下:
in main
hello in test5
hello in test5
hello in test5
hello in test5
hello in test5
当然可以修改一下,就好了:
func main() {
for i:=0; i<5; i++ {
defer func(i int) {
fmt.Println("hello in test" + strconv.Itoa(i))
}(i)
}
fmt.Println("in main")
}
上面主要原因是把一个闭包给解除掉了,使得defered函数不在使用外部变量,而是使用传递进来的参数,这样不再是一个闭包。
3.3. defer函数放回值是函数
func ou() func() func() {
fmt.Println("outside")
return func() func() {
fmt.Println("inside")
return func() {
fmt.Println("inside inside")
}
}
}
func main() {
defer ou()()()
fmt.Println("here")
}
结果如下:
outside
inside
here
inside inside
defer的声明对象必须是一个函数,如果被defer的函数放回的是一个函数,那么实际上被defer的函数是被返回的那个。可以试试将ou()()()
后面的两个括号去掉运行一下。
4. 内部原理浅析
Go的多个被defer函数的调用关系是后进先出。但这不意味着defer的内存分配是栈区,千万不要混淆。
实际上defer的内存分配一开始是堆,但是经过不断的优化之后有三种模式,具体选用哪种分配模式要根据实际代码和编译的的选择。
由于本人水平还很有限,就不进行过多深入,怕误导大家,至于哪三种模式的总结我将引用坤神的文章:
- 对于开放编码式 defer 而言:
编译器会直接将所需的参数进行存储,并在返回语句的末尾插入被延迟的调用;
当整个调用中逻辑上会执行的 defer 不超过 15 个(例如七个 defer 作用在两个返回语句)、总 defer 数量不超过 8 个、且没有出现在循环语句中时,会激活使用此类 defer;
此类 defer 的唯一的运行时成本就是存储参与延迟调用的相关信息,运行时性能最好。- 对于栈上分配的 defer 而言:
编译器会直接在栈上记录一个 _defer 记录,该记录不涉及内存分配,并将其作为参数,传入被翻译为 deferprocStack 的延迟语句,在延迟调用的位置将 _defer 压入 Goroutine 对应的延迟调用链表中;
在函数末尾处,通过编译器的配合,在调用被 defer 的函数前,调用 deferreturn,将被延迟的调用出栈并执行;
此类 defer 的唯一运行时成本是从 _defer 记录中将参数复制出,以及从延迟调用记录链表出栈的成本,运行时性能其次。- 对于堆上分配的 defer 而言:
编译器首先会将延迟语句翻译为一个 deferproc 调用,进而从运行时分配一个用于记录被延迟调用的 _defer 记录,并将被延迟的调用的入口地址及其参数复制保存,入栈到 Goroutine 对应的延迟调用链表中;
在函数末尾处,通过编译器的配合,在调用被 defer 的函数前,调用 deferreturn,从而将 _defer 实例归还到资源池,而后通过模拟尾递归的方式来对需要 defer 的函数进行调用。
此类 defer 的主要性能问题存在于每个 defer 语句产生记录时的内存分配,记录参数和完成调用时的参数移动时的系统调用,运行时性能最差。
5. 小结
- 使用defer会产生一定的开销,虽然已经进行的很多优化,但是还是尽量不要使用
- 注意闭包和函数作为参数的问题
参考文献:
golang defer原理
坤神文章
撩我?
可以搜索我的公众号:Kyda