文章目录
1.什么是defer?
defer是Go语言提供的一种用于注册延迟调用的机制;让函数或语句可以在当前函数执行完毕后(包括通过
return
正常结束或者panic
导致的异常结束)执行。
defer语句通常用于一些成对操作的场景:打开连接/关闭连接;加锁/释放锁;打开文件/关闭文件等。
2.defer进阶
2.1. defer的底层原理是什么
每次defer语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来;当外层函数(非代码块,如一个for循环)退出时,defer函数按照定义的逆序执行;如果defer执行的函数为nil,那么会在最终调用函数的产生panic。
defer语句并不会马上执行,而是会进入一个栈,函数return
前,会按先进后出的顺序执行。也就是说最先被定义的defer
语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面先执行,那后面函数的依赖就没有了。
defer函数定义时,对外部变量的引用是有两种方式的,分别是作为函数参数和作为闭包引用。作为函数参数,则在defer定义时就把值传递给defer,并被cache起来;作为闭包引用的话,则会在defer函数真正调用时候根据整个上下文确定当前的值。
2.2.defer命令拆解
return xxx
上面这条语句经过编译之后,变成了三条指令:
1.返回值 = xxx
2.调用defer函数
3.空的return
1,3步才是return
语句真正的命令,第2步是defer定义的语句,这里可能会操作返回值。我们来看两个个例子,试着将return
语句和defer
语句拆解到正确的顺序。
第一个例子:
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
拆解后:
func f() (r int) {
t := 5
// 返回值等于t
r = t
// 执行defer语句
func() {
t = t + 5
}
// 空的return
return
}
这里没有第二步操作返回值r,因此,main函数中调用f()得到5 。
第二个例子:
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
拆解后:
func f() (r int) {
// 返回值 = 1
r = 1
// 执行defer语句
func(r int) {
r = r + 5
}
// 空的return
return
}
需要注意的是,这里的defer语句是传参调用,参数r会被先缓存下来,所以第二步中的副本并不会影响到返回值,最后返回值仍然是1。
3.更多知识点
3.1.很多有返回值的内置函数是不能被延迟调用的
在Go中,自定义函数的调用的返回结果都可以被舍弃。但是,大多数内置函数(除了copy
和recover
)的调用的返回结果都不可以舍弃(至少对于Go1.13来说)。另一方面,我们已经了解到延迟函数调用的所有返回结果都舍弃掉。所以,很多内置函数是不能被延迟调用的。
幸运的是,在实践中,延迟调用内置函数的需求很少见。通常,只有append
函数有可能会需要被延迟调用。对于这种情形,我们可以延迟调用一个调用了append
函数的匿名函数来满足这个需求。
package main
import "fmt"
func main() {
s := []string{"a","b","c","d"}
defer fmt.Println(s)
defer append(s[:],"x","y") // 编译错误
defer func() {
_ = append(s[:],"x","y")
}
}
3.2.延迟调用的函数值的估值时刻
一个被延迟调用的函数值可能是一个nil函数值。这种情形将导致一个恐慌。对于这种情形,恐慌产生在此延迟调用被推入延迟调用堆之前。
package main
import "fmt"
func main() {
defer fmt.Println("reachable")
var f func() // f == nil
defer f() // 将产生一个恐慌
// 下面的代码不会被执行
fmt.Println("not reachable")
f = func() {}
}