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
以下是你所指内容的更详细版本,并附带了更多上下文信息:
我运行了一个程序来处理前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 之前,任何循环变量(包括接口值)在闭包中使用时都需要复制到局部变量。

