Go Wiki:常见错误

目录

简介

当新的程序员开始使用 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://golang.ac.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 的一部分。