golang中defer的不解

烧麦
  • 200

最近在学go的defer,发现以下两个例子,但是不明白区别是什么?

package main

import "fmt"

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(t.name, " closed")
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer t.Close()
    }
} 

输出结果:

c closed
c closed
c closed

然后我们加一句代码:

package main

import "fmt"

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(t.name, " closed")
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        t2 := t  //定义新变量t2 t赋值给t2
        defer t2.Close()
    }
} 

结果是:

c closed
b closed
a closed

不知道哪位懂这个区别的不吝赐教,本人初学go语言。感谢!

回复
阅读 2.5k
4 个回答
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer t.Close()
    }
}

其实等价与

func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    t := Test{}
    for _, t = range ts {
        defer t.Close()
    }
}

因为for循环之后,t == Test{"c"},所以最后t.Close输出的都是c

func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        t2 := t  //定义新变量t2 t赋值给t2
        defer t2.Close()
    }
}

等价于

func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    t := Test{}
    for _, t = range ts {
        t2 := t  //定义新变量t2 t赋值给t2
        defer t2.Close()
    }
}

这个代码里面,分别创建了3个t2,分别为a b c,所以分别调用Close,输出了a b c

for循环变量就一个,defer t.Close() 用的是同一个t,3 个 defer 函数都会在函数结束返回前调用,这是 t 已经是"c"了。第二段代码:

t2 := t
defer t2.Close()

这种方式相当于每次循环创建一个不同的变量t2,调用t2Close()方法。

其实不光是defer函数了,所有异步的,引用循环变量的情况都有问题,例如在循环中启动 goroutine:

for i := 0; i < 10; i++ {
  go func() {
    fmt.Println(i)
  }()
}

上面程序一般都会输出 10 个 10,而不是1-10 10个数字,因为循环变化 i 只有一个,每个 goroutine 都引用的这个 i,而循环退出时 i 变成10 了。

解决方法也很直接,和你上面的一样:

for i := 0; i < 10; i++ {
  i2 := i
  go func() {
    fmt.Println(i2)
  }()
}

每次循环创建一个新的变量i2,每个 goroutine 引用的都是一个不同的i2

还有一种方法,将循环变量作为参数:

for i := 0; i < 10; i++ {
  go func(i int) {
    fmt.Println(i)
  }(i)
}

goroutine 的参数是调用时就计算的,而int是穿值的。

所以上面你的程序中也可以写成:

for _, t := range ts {
   defer func (t Test) {
     t.Close()
   }(t)
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        fmt.Printf("&t:%p\n", &t)
        t2 := t
        fmt.Printf("$t2:%p\n", &t2)
        defer t.Close()
    }
}

打印结果:

&t:0xc000088230
$t2:0xc000088240
&t:0xc000088230
$t2:0xc000088250
&t:0xc000088230
$t2:0xc000088260
c closed
c closed
c closed

个人见解:
虽然 defer 后的延迟函数参数在创建时就确定了,代码中可以看出用的是同一个变量,,地址都为一个,t2 的话就是不同的变量,每个地址都不一样

@叶东富 的回答的确不错, 但我仔细看了一下, 这个回答的确有问题. 首先变量就是在 for 循环作用于, 不会出现提前声明一个. 我们把代码换成.

package main

import (
    "fmt"
    "unsafe"
)

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(unsafe.Pointer(t), t.name, " closed")
}
func main() {
    ts := []*Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {

        defer t.Close()
    }
}

// output 正常, 我加了一个指针打印
0xc000044260 c  closed
0xc000044250 b  closed
0xc000044240 a  closed

这个问题归根结底是: 值上直接调用指针方法
这是你的代码, 我加上一个地址打印:

package main

import (
    "fmt"
    "unsafe"
)

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(unsafe.Pointer(t), t.name, " closed")
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {

        fmt.Println(unsafe.Pointer(&t))
        defer t.Close()
    }
}



// output, 可以看到打印的地址都是同一个
0xc000044240
0xc000044240
0xc000044240
0xc000044240 c  closed
0xc000044240 c  closed
0xc000044240 c  closed

从输出大概就能看到为什么了, 引用官方的一段原话

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.
There is a handy exception, though. When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically
值方法(value methods)可以通过指针和值调用,但是指针方法(pointer methods)只能通过指针来调用。
但有一个例外,如果某个值是可寻址的(addressable,或者说左值),那么编译器会在值调用指针方法时自动插入取地址符,使得在此情形下看起来像指针方法也可以通过值来调用

当你通过一个值去调用指针方法, 那么会去寻址, 而你在循环中调用

  1. 第一次: 那么这个变量开始地址是: 0xc000044240, 这时候指针调用的方法Close也是记住了这个地址, 指针指向结构体的值是a, 循环结束释放局部变量
  2. 第二次: 那么这个变量开始地址还是: 0xc000044240, 这时候指针调用的方法Close也是记住了这个地址, 指针指向结构体的值是b, 循环结束释放局部变量
  3. 第三次: 那么这个变量开始地址还是: 0xc000044240, 这时候指针调用的方法Close也是记住了这个地址, 指针指向结构体的值是c,
    所以最后输出都是c
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
你知道吗?

宣传栏