Go中的循环变量和闭包

最近在写goroutine碰到了常见错误

for _, val := range values {
    go func() {
        fmt.Println(val)
    }()
}

在这里,实际结果并不是预期的所有值都输出一遍(而是出现data race),FAQ里给出了两种解决方案:

// 方案1
for _, val := range values {
    go func(val interface{}) {
	fmt.Println(val)
    }(val)
}

// 方案2
for _, val := range values {
    val := val
    go func() {
	fmt.Println(val)
    }()
}

出现上述现象主要涉及以下几个问题:

循环变量

常见的for循环语法for (initStmt; condition; postStmt) block等价于

{
    initStmt;
    while (codition) {
        block;
        postStmt;
    }
}

这里暗含了一个条件,那就是循环变量在迭代过程中是同一个, 这在Go语言中也不例外(For statements)。
这就导致了开头的问题,由于goroutine什么时候执行是不确定的,也就是说这里val的值取决于goroutine执行的时候循环变量val的值。
如果主函数先返回,那么最终结果都是最后一次循环的循环变量。

闭包

闭包主要包含两部分内容,一个函数,以及函数内自由变量的绑定。在开头的例子中,每一个匿名函数

func() {
    fmt.Println(val)
}

都创建了一个闭包,闭包内的val都指向循环变量val

闭包查找自由变量依照作用域向上查找,因此,在方案2里面,自由变量val指向循环内的临时变量val := val, 此时每个goroutine里val的值为每个迭代内的值。

传值调用

Go里面只有传值调用(call by value),也就是每次调用都会复制参数的值,对于指针而言,同样会传一个新的指针,不过指向的内容是一样的。

type A struct {
    a int
}

func f(a *A) {
    fmt.Printf("%p\n", a) // addr1
    fmt.Printf("%p\n", &a) // addr3
}

func main() {
    a := &A{a: 3}
    fmt.Printf("%p\n", a) // addr1
    fmt.Printf("%p\n", &a) // addr2
    f(a)
}

因此, 在方案1里面,每个迭代中val当前的值都被复制到了匿名函数的栈上,此时和循环变量没有任何关系。

range里的循环变量

FAQ里面提到循环中使用同一个变量可能是一个设计错误,这个问题类似的也出现在C#中(foreach),

This behavior of the language, not defining a new variable for each iteration, may have been a mistake in retrospect. It may be addressed in a later version but, for compatibility, cannot change in Go version 1.

C#选择在foreach中复制循环变量,但是在普通的for循环中保留共用变量,后者可能更多的是出于兼容性的保证。不知道后续Go会怎么处理这个“可能”的设计失误。

posted @ 2023-01-02 20:39  Christophe1997  阅读(134)  评论(0编辑  收藏  举报