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会怎么处理这个“可能”的设计失误。