Go 语言里的 defer 参数传值问题
使用 defer 时遇到的一些问题,专门记录一下。
先说结论,defer 函数时,所有变量是按值(Go 语言都是传值)来暂存,等到作用域结束前执行调用。对于基于指针的方法调用,那指针里的值(即地址)会被暂存,而多次改变指针内容后 defer 的行为可能跟编码时设想的不一样。(如果是闭包,则是在后续执行时直接用到变量本身,可以读取和修改。)所以要区分变量的位置,是在栈上的,是全局,函数的参数,for 循环的变量等等;暂存的是变量的值,还是变量的地址;另外是函数还是闭包。
通过指针调用的例子:
type MyObject struct {
value int
}
func (n MyObject) F() {
tracef("n: @%p, %v", &n, n)
}
func (n *MyObject) PF() {
tracef("n: @%p, %v", n, *n)
}
func main() {
var n MyObject
tracef("n: @%p, %v", &n, n)
for _, n = range []MyObject{MyObject{123}, MyObject{456}, MyObject{789}} {
defer fmt.Println()
defer n.F() // (1)
defer n.PF() // (2)
defer func() { n.F() }() // (3)
defer func() { n.PF() }() // (4)
}
}
// Output:
// [main.main] >> n: @0xc0000100d8, {0}
// [main.(*MyObject).PF] >> n: @0xc0000100d8, {789}
// [main.MyObject.F] >> n: @0xc0000101c8, {789}
// [main.(*MyObject).PF] >> n: @0xc0000100d8, {789}
// [main.MyObject.F] >> n: @0xc000010200, {789}
//
// [main.(*MyObject).PF] >> n: @0xc0000100d8, {789}
// [main.MyObject.F] >> n: @0xc000010238, {789}
// [main.(*MyObject).PF] >> n: @0xc0000100d8, {789}
// [main.MyObject.F] >> n: @0xc000010270, {456}
//
// [main.(*MyObject).PF] >> n: @0xc0000100d8, {789}
// [main.MyObject.F] >> n: @0xc0000102a8, {789}
// [main.(*MyObject).PF] >> n: @0xc0000100d8, {789}
// [main.MyObject.F] >> n: @0xc0000102e0, {123}
在 (1) 处,栈上变量 n 的值被暂存,defer 的内容在后续执行时,通过传值赋给 MyObject.F(),其打印出来的值,跟期望的一致,而打印出来的地址,是 MyObject.F() 参数本身的地址,与栈上变量 n 无关;
在 (2) 处,栈上变量 n 的地址被暂存,defer 的内容在后续执行时,地址通过传值赋给 MyObject.PF(),其打印出来的地址,就是栈上变量 n 的地址,而打印出来的值,是当前栈上变量 n 里的内容,即循环最后一次的赋值。显然这里跟设想的不一样。
如果用 C 语言来理解和对比的话,(1) 相当于 (n).F(),而 (2) 相当于 (&n)->PF(),所以暂存的分别是 n 和 &n。对于 (2) 来说,在循环过程中,因为暂存的只是 &n 地址,所以在后面 执行时刻 看到的是通过地址 &n 所指向变量 n 里的值,而不是之前这个变量 n 里每次 defer 时刻 的值。
在 (3) 处,是在执行时,把栈上变量 n 传值赋给 (n).F(),所以打印的地址是参数变量的地址,值是循环最后一次的赋值。
在 (4) 处,是在执行时,把栈上变量 &n 传值赋给 (&n)->PF(),所以打印的地址就是栈上变量 n 的地址,而值也是循环最后一次的赋值。
作为对比:
type MyObject struct {
value int
}
func (n MyObject) F() {
tracef("n: @%p, %v", &n, n)
}
func (n *MyObject) PF() {
tracef("n: @%p, %v", n, *n)
}
func main() {
// var n MyObject
// tracef("n: @%p, %v", &n, n)
for _, n := range []MyObject{MyObject{123}, MyObject{456}, MyObject{789}} {
defer fmt.Println()
defer n.F() // (1)
defer n.PF() // (2)
defer func() { n.F() }() // (3)
defer func() { n.PF() }() // (4)
}
}
// Output:
// [main.(*MyObject).PF] >> n: @0xc000010128, {789}
// [main.MyObject.F] >> n: @0xc0000101b8, {789}
// [main.(*MyObject).PF] >> n: @0xc000010128, {789}
// [main.MyObject.F] >> n: @0xc000010200, {789}
//
// [main.(*MyObject).PF] >> n: @0xc000010120, {456}
// [main.MyObject.F] >> n: @0xc000010238, {456}
// [main.(*MyObject).PF] >> n: @0xc000010120, {456}
// [main.MyObject.F] >> n: @0xc000010270, {456}
//
// [main.(*MyObject).PF] >> n: @0xc0000100d8, {123}
// [main.MyObject.F] >> n: @0xc0000102a0, {123}
// [main.(*MyObject).PF] >> n: @0xc0000100d8, {123}
// [main.MyObject.F] >> n: @0xc0000102c8, {123}
这里 (1) 和 (2) 暂存的是 for 循环变量 n 的值和地址。从 (2) 的打印看出来,循环变量 n 每次所在位置都不一样,也就是在每次循环时在新的位置生成了一个变量,所以这样打印的结果是符合预期的。
而 (3) 和 (4),是在执行时,才把对应 defer 时的循环变量 n 及 &n 赋值并调用,变量里的值和上面 (1) (2) 一样。另外,可以看到每一次的 (2) 和 (4) 里的地址是一样的。