介绍
在 Go 语言中,函数支持匿名函数,闭包就是一种特殊的匿名函数,它可以用于访问函数体外部的变量。
需要注意的是,在 for ... range ...
中,使用 goroutine 执行闭包时,经常会掉“坑”。
因为匿名函数可以访问函数体外部的变量,而 for ... range ...
返回的 val 的值是引用的同一个内存地址的数据,所以匿名函数访问的函数体外部的 val 值是循环中最后输出的一个值。
“踩坑”示例代码
func main() { done := make(chan bool) values := []string{"a", "b", "c"} for _, v := range values { go func() { fmt.Println(v) done <- true }() } // wait for all goroutines to complete before exiting for _ = range values { <-done } }
输出结果:
c c c
阅读上面这段代码,在 for ... range ...
中,使用 goroutine 执行闭包,打印切片中的元素,实际输出结果不是我们期望得到的输出结果。
这是因为循环的每次迭代都使用相同的变量 v 实例,因此每个闭包共享该单个变量。我们可以在示例代码中简单修改,同时输出变量 v 的内存地址和值。
把 fmt.Println(v)
修改为 fmt.Printf("val=%s pointer=%p\n", v, &v)
。
修改后的输出结果:
val=c pointer=0xc000010200 val=c pointer=0xc000010200 val=c pointer=0xc000010200
我们可以在输出结果中发现,打印变量 v 的内存地址都是 0xc000010200
。
当闭包运行时,它会在执行 fmt.Println
时打印变量 v 的值,但 v 的值可能在 goroutine 启动后已被修改。感兴趣的读者朋友们可以使用 go vet
检查。
怎么避免“踩坑”呢?
一种方法是将变量作为参数传递给闭包:
func main() { done := make(chan bool) values := []string{"a", "b", "c"} for _, v := range values { go func(param string) { // fmt.Println(v) fmt.Printf("val=%s pointer=%p\n", param, ¶m) done <- true }(v) } // wait for all goroutines to complete before exiting for _ = range values { <-done } }
输出结果:
val=c pointer=0xc000010200 val=a pointer=0xc00009a000 val=b pointer=0xc0000a4000
阅读上面这段代码,通过将变量 v 的值作为参数传递给闭包,然后,该值作为形参 param 的值,在函数体内部被访问。
另外一种方法是创建一个新变量:
func main() { done := make(chan bool) values := []string{"a", "b", "c"} for _, v := range values { param := v go func() { // fmt.Println(v) fmt.Printf("val=%s pointer=%p\n", param, ¶m) done <- true }() } // wait for all goroutines to complete before exiting for _ = range values { <-done } }
输出结果:
val=c pointer=0xc000082200 val=a pointer=0xc0000821e0 val=b pointer=0xc0000821f0
通过输出结果可以发现,该种方式也可以达到我们期望的结果。
03
总结
本文我们介绍了在 for ... range ...
中,Go 语言在每次迭代时,没有定义一个新变量,导致使用 goroutine 运行闭包时,经常会掉“坑”。
我们给出避免“踩坑”的两种方法,其中,第二种方法更简单。
推荐阅读:
参考资料: