Featured image of post Go 中循环使用 defer 的一个 bug

Go 中循环使用 defer 的一个 bug

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

可以看到输出了三个c close
当时的高赞回答是, 这样子使用defer会声明一个外部变量, 循环中不断赋值, 导致用了最后一个, 但我看了一下代码, 感觉不对. 就自己去试了一下.


最后确定是值调用指针方法的问题, 把测试代码换成.

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
本作品采用知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,可适当缩放并在引用处附上图片所在的文章链接。