在Golang使用defer常常会迷惑于以下两个问题
defer
关键字的调用时机以及多次调用defer
时执行顺序是如何确定的;defer
关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果;
defer
的调用时机是在当前函数返回之前调用的,执行顺序是先入后出。
预计算参数
defer
也是传参的。比如说以下代码就会输入0,而不是最终的1
n := 0
defer fmt.Println(n)
n = 1
其原因在于defer
会立刻拷贝defer函数中引用的外部参数,也就是说运行到defer fmt.Println(n)
时,n的值便已经在defer函数里面啦
即使defer的是个有参方法,也会拷贝到参数
func TestDeferPassParmFunc(t *testing.T) { n := 0 defer deferPassParam(n) n = 1 } func deferPassParam(r int) { fmt.Println(r) } // output: // 0
如果想避免这种情况我们可以defer匿名函数或者指针参数函数
func TestDeferPassParmFunc(t *testing.T) {
n := 0
defer deferPassParam(&n)
n = 1
}
func deferPassParam(r *int) {
fmt.Println(*r)
}
// output:
// 1
常见defer现象
连续defer,先入后出
按照先入后出执行
defer fmt.Println("one")
defer fmt.Println("two")
defer fmt.Println("three")
// output:
// three
// two
// one
defer声明,变量确认
变量在defer声明时便已经确认,即使后面变量发生改变,defer实际执行时也与声明时保持一致。
n := 0
defer fmt.Println(n)
n = 1
// output:
// 0
defer闭包,参数可修改
对于有名返回值,被defer的函数是闭包,且该函数为命名返回值,那么defer是可以访问返回值,并修改
func namedReturn() (r int) {
defer func() {
r = 1
}()
return 2
}
// output
// 1
当然,如果被defer的函数不是闭包或匿名,但是只要传入有名返回值的指针,其实还是可以的
func namedReturnWithDeferNamedFunc() (r int) { defer namedFuncForDer(&r) return 2 } func namedFuncForDer(r *int) { *r = 1 } // output // 1
除了命名返回值这种特殊参数外,其他普通参数其他也可访问修改
// 无参匿名
n := 1
defer func() {
fmt.Println(n)
}()
n = n + 1
return n
// output:
// 2
// 有参匿名
n := 1
defer func(x int) {
fmt.Println(n)
}(n)
n = n + 1
return n
// output:
// 2
总结
- defer按照栈顺序,先定义的后执行
- defer闭包会在defer执行时,获取或改变闭包内变量(但不改变非命名返回值)
- defer匿名函数也会获取或者改变闭包内变量(但不改变非命名返回值)
- defer普通函数,在非指针参数情况下,与defer声明时变量值一致
Ref
- https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer