延迟语句的执行顺序是什么
先看一下官方文档对 defer 的解释:
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed. (每次 defer 语句执行的时候,会把函数 “压栈”,函数参数会被复制下来; 当外层函数(注意不是代码块,如一个 for 循环块并不是外层函数)退出时,defer 函数按照定义的顺序逆序执行;如果 defer 执行的函数为 nil,那么会在最终调用函数的时候产生 panic。)
defer 语句并不会马上执行,而是会进入一个栈,函数 return 前,会按先进后出的顺序执行。也就是说,最先被定义的 defer 语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面先执行了,那后面函数的依赖就没有了,因而可能会出错。
在 defer 函数定义时,对外部变量的引用有两种方式:函数参数、闭包引用。前者在 defer 定义时就把值传递给 defer,并被 cache 起来;后者则会在 defer 函数真正调用时根据整个上下文确定参数当前的值。
defer 后面的函数在执行的时候,函数调用的参数会被保存起来,也就是复制了一份。真正执行的时候,实际上用到的是这个复制的变量,因此如果此变量是一个 “值”,那么就和定义的时候是一致的。如果此变量是一个 “引用”,那就可能和定义的时候不一致。
举个例子:
package main
import "fmt"
func main() {
var whatever [3]struct{}
// 这种方式就是引用
for i := range whatever {
defer func() {
fmt.Println(i)
}()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
执行结果:
2
2
2
2
3
defer 后面跟的是一个闭包(后面小节会讲到),i 是 “引用” 类型的变量,for 循环结束后 i 的值为 2,因此最后打印了 3 个 2。
有了上面的基础,再来看一个例子:
package main
import "fmt"
func main() {
var n number
defer n.print()
defer n.pprint()
defer func() { n.print() }()
defer func() { n.pprint() }()
n = 3
}
type number int
func (n number) print() { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
执行结果是:
3
3
3
0
2
3
4
注意,defer 语句的执行顺序和定义的顺序相反。
第四个 defer 语句是闭包,引用外部函数的 n,最终结果是 3;第三个 defer 语句同上;第二 个 defer 语句, n 是引用, 最终求值是 3; 第一个 defer 语句, 对 n 直接求值, 开始的时候 n=0,所以最后是 0。
我们再来看两个延伸情况。例如,下面的例子中,return 之后的 defer 语句会执行吗?
func main() {
defer func() {
fmt.Println("before return")
}()
if true {
fmt.Println("during return")
return
}
defer func() {
fmt.Println("after return")
}()
}
2
3
4
5
6
7
8
9
10
11
12
运行结果:
during return
before return
2
解析:return 之后的 defer 函数不能被注册,因此不能打印出 after return。
第二个延伸示例则可以视为对 defer 的原理的利用。某些情况下,会故意用到 defer 的 “先求 值,再延迟调用” 的性质。想象这样的场景:在一个函数里,需要打开两个文件进行合并操作,合 并完成后,在函数结束前关闭打开的文件句柄。
func mergeFile() error {
// 打开文件一
f, _ := os.Open("file1.txt")
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close file1.txt err %v\n", err)
}
}(f)
}
// 打开文件二
f, _ = os.Open("file2.txt")
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close file2.txt err %v\n", err)
}
}(f)
}
// ....
return nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上面的代码中就用到了 defer 的原理,defer 函数定义的时候,参数就已经复制进去了,之后,真正执行 close () 函数的时候就刚好关闭的是正确的 “文件” 了,很巧妙。如果不这样,将 f 当成函数参数传递进去的话,最后两个语句关闭的就是同一个文件了:都是最后一个打开的文件。
在调用 close () 函数的时候,要注意一点:先判断调用主体是否为空,否则可能会解引用了一个空指针,进而 panic。