Go Wiki:常见错误
目录
引言
当新 Go 程序员开始使用 Go,或者老 Go 程序员开始使用新概念时,会犯一些常见的错误。下面是一些在邮件列表和 IRC 中经常出现的、不详尽的常见错误列表。
使用循环迭代器变量的引用
注意:以下部分适用于 Go < 1.22。Go 版本 >= 1.22 使用作用域限定到迭代的变量,有关详细信息,请参阅 修复 Go 1.22 中的 for 循环。
在 Go 中,循环迭代器变量是一个在每次循环迭代中取不同值的单个变量。这非常高效,但如果使用不当可能会导致意外行为。例如,请看以下程序
func main() {
var out []*int
for i := 0; i < 3; i++ {
out = append(out, &i)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
}
它将输出意外的结果
Values: 3 3 3
Addresses: 0x40e020 0x40e020 0x40e020
解释:在每次迭代中,我们都将 i
的地址追加到 out
切片中,但由于它是一个相同的变量,我们追加的是相同的地址,该地址最终包含的是赋给 i
的最后一个值。解决方案之一是将循环变量复制到一个新变量中
for i := 0; i < 3; i++ {
+ i := i // Copy i into a new variable.
out = append(out, &i)
}
程序的输出现在符合预期
Values: 0 1 2
Addresses: 0x40e024 0x40e028 0x40e032
解释:i := i
这行代码将循环变量 i
复制到一个新变量中,该变量的作用域限定在 for 循环体块内,也称为 i
。新变量的地址被追加到数组中,使其生命周期超出 for 循环体块。每次循环迭代都会创建一个新变量。
虽然这个例子可能看起来有点明显,但在其他一些情况下,相同的意外行为可能更隐蔽。例如,循环变量可以是数组,而引用可以是切片
func main() {
var out [][]int
for _, i := range [][1]int{{1}, {2}, {3}} {
out = append(out, i[:])
}
fmt.Println("Values:", out)
}
输出
Values: [[3] [3] [3]]
当循环变量在 Goroutine 中使用时(请参阅下一节),也会出现同样的问题。
在循环迭代器变量上使用 goroutine
注意:以下部分适用于 Go < 1.22。Go 版本 >= 1.22 使用作用域限定到迭代的变量,有关详细信息,请参阅 修复 Go 1.22 中的 for 循环。
在 Go 中进行迭代时,可能会尝试使用 goroutine 并行处理数据。例如,您可能会这样编写,使用闭包
for _, val := range values {
go func() {
fmt.Println(val)
}()
}
上面的 for 循环可能不会如您所料地运行,因为它们的 val
变量实际上是一个在每次迭代中获取切片元素值的单个变量。由于所有的闭包都只绑定到这一个变量,当您运行此代码时,很可能会看到每次迭代都打印出最后一个元素,而不是按顺序打印每个值,因为 goroutine 很可能在循环结束后才会开始执行。
编写该闭包循环的正确方法是
for _, val := range values {
go func(val interface{}) {
fmt.Println(val)
}(val)
}
通过将 val 作为闭包的参数,val
在每次迭代时都会被求值并放在 goroutine 的堆栈上,因此当 goroutine 最终执行时,每个切片元素都可用。
同样需要注意的是,在循环体内部声明的变量在迭代之间不会共享,因此可以在闭包中独立使用。以下代码使用了一个通用的索引变量 i
来创建独立的 val
,从而实现了预期的行为
for i := range valslice {
val := valslice[i]
go func() {
fmt.Println(val)
}()
}
请注意,如果不在 goroutine 中执行此闭包,代码会按预期运行。以下示例将打印出 1 到 10 之间的整数。
for i := 1; i <= 10; i++ {
func() {
fmt.Println(i)
}()
}
即使所有的闭包仍然闭合在同一个变量(在本例中是 i
),它们也会在变量改变之前执行,从而产生期望的行为。 https://go-lang.org.cn/doc/faq#closures_and_goroutines
您可能会遇到另一种类似的情况,如下所示
for _, val := range values {
go val.MyMethod()
}
func (v *val) MyMethod() {
fmt.Println(v)
}
上面的示例也会打印 values 的最后一个元素,原因与闭包相同。要解决这个问题,请在循环内部声明另一个变量。
for _, val := range values {
newVal := val
go newVal.MyMethod()
}
func (v *val) MyMethod() {
fmt.Println(v)
}
此内容是 Go Wiki 的一部分。