Golang中关于GitHub近期讨论"重定义for循环变量语义"示例的问题

Golang中关于GitHub近期讨论"重定义for循环变量语义"示例的问题 在最近关于重新定义 for 循环变量语义的讨论中,Rob 举了一个例子,他认为当循环变量是接口值时,复制该变量是不必要的。为了方便起见,将示例和 Rob 的陈述摘录如下:

	for _, informer := range c.informerMap {
+		informer := informer
		go informer.Run(stopCh)
	}

Rob 说:

这两个更改中有一个是不必要的,另一个是真正的错误修复,但如果没有更多上下文,你无法分辨哪个是哪个。(在其中一个中,循环变量是一个接口值,复制它没有效果;

实际上,在我看来,即使循环变量是接口值,复制它仍然是必要的。我基于以下代码进行了测试。

type Inf interface {
	Get()
}


type S struct {
	name string
}

func (s *S) Get()  {
	fmt.Println(s)
	return
}

func main() {
	infs := []Inf{&S{name: "Alice"}, &S{name: "Bob"}}

	var wg sync.WaitGroup
    // i 是一个接口值
	for _, i := range infs {
		// i := i
		wg.Add(1)
		go func() {
			defer wg.Done()
			i.Get()
		}()
	}
	wg.Wait()
}

如果不将循环变量复制到局部变量,输出结果如下,这是错误的。

&{Bob}
&{Bob}

我可能误解了 Rob 的意思。您能帮我解释一下“循环变量是一个接口值,复制它没有效果”这句话是什么意思吗?


更多关于Golang中关于GitHub近期讨论"重定义for循环变量语义"示例的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

以下是你所指内容的更详细版本,并附带了更多上下文信息:

我运行了一个程序来处理前14k个模块的git日志,这些模块来自大约12k个git仓库,并寻找那些差异块完全由添加“x := x”行组成的提交。我发现了大约600个这样的提交。仔细检查后,大约一半的更改是不必要的,可能是由于不准确的静态分析、对语义的混淆或过度谨慎导致的。也许最引人注目的是来自不同项目的这一对更改:

	for _, informer := range c.informerMap {
+		informer := informer
		go informer.Run(stopCh)
	}
	for _, a := range alarms {
+		a := a
		go a.Monitor(b)
	}

这两个更改中,有一个是不必要的,而另一个则是真正的错误修复,但如果没有更多上下文,你无法分辨哪个是哪个。(在一种情况下,循环变量是一个接口值,复制它没有效果;在另一种情况下,循环变量是一个结构体,并且方法使用指针接收器,因此复制它可以确保每次迭代的接收器都是不同的指针。)

我认为这是一个展示问题的例子:The Go Play Space(在普通的Go Playground上也有同样的内容:Go Playground - The Go Programming Language)。

由于Russ Cox所说的“对语义的混淆”(也就是的混淆),我花了几天时间才把它整理出来。

认为这个例子展示了问题,但我必须承认,我实际上并不理解它是如何工作的 :disappointed:。也许(???)这是因为从接口方法调用启动的goroutine通过值“捕获”或“闭包”了接口,而从非接口方法调用启动的goroutine则通过引用捕获了值?

更多关于Golang中关于GitHub近期讨论"重定义for循环变量语义"示例的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Rob Pike 的评论需要结合具体上下文理解。他指的是当循环变量本身是接口值时,复制这个接口值(即 informer := informer)并不会复制底层数据,因为接口值已经是两个字段的包装(类型指针和数据指针)。但你的测试展示了另一个关键点:闭包捕获的是循环变量本身,而不是它当时的值。

在你的例子中:

for _, i := range infs {
    // i 是接口值,包含 (type=*S, data=指向某个 S 的指针)
    go func() {
        i.Get() // 闭包捕获的是变量 i 本身
    }()
}

所有 goroutine 共享同一个循环变量 i,在每次迭代中 i 被更新为新的接口值。由于 goroutine 执行延迟,它们最终读取的是 i 最终的值(即 &{Bob})。

Rob 所说的“复制没有效果”是指这种情况:

for _, informer := range c.informerMap {
    informer := informer // 复制接口值
    go informer.Run(stopCh)
}

这里的 informer 是接口值,复制它只是复制了接口的两个字段,而不是底层数据。但复制后,每个 goroutine 使用自己独立的接口值变量,避免了闭包捕获问题。

实际上,复制接口值是必要的,因为它创建了新的变量实例。Rob 可能是在强调:复制接口值不会复制底层对象(例如切片或结构体),但对于解决闭包捕获问题已经足够。

验证示例:

type Inf interface{ Get() }

type S struct{ name string }

func (s *S) Get() { fmt.Println(s.name) }

func main() {
    infs := []Inf{&S{"Alice"}, &S{"Bob"}}
    var wg sync.WaitGroup
    
    for _, i := range infs {
        i := i // 复制接口值(创建新变量)
        wg.Add(1)
        go func() {
            defer wg.Done()
            i.Get() // 每个 goroutine 捕获不同的 i 变量
        }()
    }
    wg.Wait()
}

输出:

Alice
Bob

总结:Rob 的“复制没有效果”可能被误解了。他是指复制接口值不会复制底层数据,但创建新变量对解决闭包捕获问题是有效的。在 Go 1.22 之前,任何循环变量(包括接口值)在闭包中使用时都需要复制到局部变量。

回到顶部