绝大多数Gopher喜欢使用defer,因为它让函数变得简洁且健壮。但“工欲善其事,必先利其器”,要想用defer,就需要提前了解几个关于defer的关键问题,以避免掉进一些不必要的“坑”。
func bar() (int, int) {
return 1, 2
}
func foo() {
var c chan int
var sl []int
var m = make(map[string]int, 10)
m["item1"] = 1
m["item2"] = 2
var a = complex(1.0, -1.4)
var sl1 []int
defer bar()
defer append(sl, 11)
defer cap(sl)
defer close(c)
defer complex(2, -2)
defer copy(sl1, sl)
defer delete(m, "item2")
defer imag(a)
defer len(sl)
defer make([]int, 10)
defer new(*int)
defer panic(1)
defer print("hello, defer\n")
defer println("hello, defer")
defer real(a)
defer recover()
}
func main() {
foo()
}
运行该示例:
./deferred_func_6.go:22:2: defer discards result of append(sl, 11)
./deferred_func_6.go:23:2: defer discards result of cap(sl)
./deferred_func_6.go:25:2: defer discards result of complex(2, -2)
./deferred_func_6.go:28:2: defer discards result of imag(a)
./deferred_func_6.go:29:2: defer discards result of len(sl)
./deferred_func_6.go:30:2: defer discards result of make([]int, 10)
./deferred_func_6.go:31:2: defer discards result of new(*int)
./deferred_func_6.go:35:2: defer discards result of real(a)
Go编译器给出了一组错误提示!从中我们看到,append、cap、len、make、new等内置函数是不可以直接作为deferred函数的,而close、copy、delete、print、recover等可以。
对于那些不能直接作为deferred函数的内置函数,我们可以使用一个包裹它的匿名函数来间接满足要求。以append为例:
defer func() {
_ = append(sl, 11)
}()
但这么做有什么实际意义需要开发者自己把握。
2. 把握好defer关键字后表达式的求值时机
牢记一点,defer关键字后面的表达式是在将deferred函数注册到deferred函数栈的时候进行求值的。
下面用一个典型的例子来说明defer关键字后表达式的求值时机:
func foo1() {
for i := 0; i <= 3; i++ {
defer fmt.Println(i)
}
}
func foo2() {
for i := 0; i <= 3; i++ {
defer func(n int) {
fmt.Println(n)
}(i)
}
}
func foo3() {
for i := 0; i <= 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}
func main() {
fmt.Println("foo1 result:")
foo1()
fmt.Println("\nfoo2 result:")
foo2()
fmt.Println("\nfoo3 result:")
foo3()
}
我们逐一分析foo1、foo2和foo3中defer关键字后的表达式的求值时机:
在foo1中,defer后面直接接的是fmt.Println函数,每当defer将fmt.Println注册到deferred函数栈的时候,都会对Println后面的参数进行求值。根据上述代码逻辑,依次压入deferred函数栈的函数是:
fmt.Println(0)
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)
因此,在foo1返回后,deferred函数被调度执行时,上述压入栈的deferred函数将以
LIFO次序出栈执行,因此输出的结果为:
3
2
1
0
在foo2中,defer后面接的是一个带有一个参数的匿名函数。每当defer将匿名函数注
册到deferred函数栈的时候,都会对该匿名函数的参数进行求值。根据上述代码逻辑,依
次压入deferred函数栈的函数是:
func(0)
func(1)
func(2)
func(3)
因此,在foo2返回后,deferred函数被调度执行时,上述压入栈的deferred函数将以
LIFO次序出栈执行,因此输出的结果为:
3
2
1
0
在foo3中,defer后面接的是一个不带参数的匿名函数。根据上述代码逻辑,依次压入
deferred函数栈的函数是:
func()
func()
func()
func()
因此,在foo3返回后,deferred函数被调度执行时,上述压入栈的deferred函数将以
LIFO次序出栈执行。匿名函数以闭包的方式访问外围函数的变量i,并通过Println输出i的
值,此时i的值为4,因此foo3的输出结果为:
4
4
4
4
鉴于defer表达式求值时机十分重要,我们再来看一个例子:
func foo1() {
sl := []int{1, 2, 3}
defer func(a []int) {
fmt.Println(a)
}(sl)
sl = []int{3, 2, 1}
_ = sl
}
func foo2() {
sl := []int{1, 2, 3}
defer func(p *[]int) {
fmt.Println(*p)
}(&sl)
sl = []int{3, 2, 1}
_ = sl
}
func main() {
foo1()
foo2()
}
我们分别分析一下这个示例中的foo1、foo2函数。
在foo1中,defer后面的匿名函数接收一个切片类型参数,当defer将该匿名函数注册到deferred函数栈的时候,会对它的参数进行求值,此时传入的变量sl的值为[]int{1, 2,3},因此压入deferred函数栈的函数是:
func([]int{1,2,3})
之后虽然sl被重新赋值,但是在foo1返回后,deferred函数被调度执行时,deferred函数的参数值依然为[]int{1, 2, 3},因此foo1输出的结果为[1 2 3]。
在foo2中,defer后面的匿名函数接收一个切片指针类型参数,当defer将该匿名函数注册到deferred函数栈的时候,会对它的参数进行求值,此时传入的参数为变量sl的地址,因此压入deferred函数栈的函数是:
func(&sl)之后虽然sl被重新赋值,但是在foo2返回后,deferred函数被调度执行时,deferred函数的参数值依然为sl的地址,而此时sl的值已经变为[]int{3, 2, 1},因此foo2输出的结果为[3 2 1]。