team leader 发现一个Golang程序的bug,是由不正确使用闭包引起。记载一下,以作备忘。
猜猜一下程序的结果:
1 package main2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func main() { 9 arr := []int{1, 2, 3, 4, 5, 6} 10 for _, num := range arr { 11 go func() { 12 fmt.Println(num) 13 14 }() 15 } 16 17 time.Sleep(1 * time.Second) 18 19 } ~
闭包导致的结果如下:
dill@ubuntu-vm:~/GoProjcet/src/exercise$ go run closure.go 6 6 6 6 6 6
避免闭包:
1 package main 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func main() { 9 arr := []int{1, 2, 3, 4, 5, 6} 10 for _, num := range arr { 11 go func(n int) { 12 fmt.Println(n) 13 14 }(num) 15 } 16 17 time.Sleep(1 * time.Second) 18 19 }
得到想要的结果:
dill@ubuntu-vm:~/GoProjcet/src/exercise$ go run closure.go 1 2 3 4 5 6
说到原因,还要从匿名函数说起。
什么是匿名函数?
命名函数只能在包水平声明,但是我们可以在任意表达式中使用函数字面量的形式给一个函数赋值(相当于在函数中声明函数,不过该函数没有名字,作用域只在表达式内)。函数字面量很像函数声明的,但是在func关键字后没有跟谁函数名字。它是一个表达式,他的值被称为匿名函数。函数字面量使我们可以在用到的地方定义一个函数(随用随定义)。重点是,这种方式定义的函数,可以进入整个lexical 环境,以至于内部函数可以引用外围函数的变量。
本例中,变量单一num被所有的匿名函数共享,即go生成的所有协程均可访问到num,不幸的是num被随后的循环迭代更新。等新goroutinues开始执行时,循环已经更新num,开始下一轮迭代,甚至已经轮询完成。所以当这些goroutine读取num的值时,惊奇的发现,num已经变成了slice中的最后一个值。通过添加一个明确的参数,我们保证我们使用的值是当前go程序正在执行的值。