Go语言的闭包捕获的外部变量,我还是习惯以Lua的叫法,称之为Upvalue,毕竟Go借鉴了很多Lua的特性。
让我们首先看五个几乎一样的代码片段。
package main
import (
"log"
"sync"
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg sync.WaitGroup, i int) {
log.Printf("i:%d", i)
wg.Done()
}(wg, i)
}
wg.Wait()
log.Println("exit")
}
输出:
go run wgtest1.go
2017/01/01 23:43:08 i:4
2017/01/01 23:43:08 i:2
2017/01/01 23:43:08 i:3
2017/01/01 23:43:08 i:1
2017/01/01 23:43:08 i:0
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc42000a2ac)
/usr/local/Cellar/go/1.7.4_1/libexec/src/runtime/sema.go:47 +0x30
sync.(*WaitGroup).Wait(0xc42000a2a0)
/usr/local/Cellar/go/1.7.4_1/libexec/src/sync/waitgroup.go:131 +0x97
main.main()
/Users/linkerlin/gos/wgtest1.go:17 +0xba
exit status 2
这是因为Go语言中WaitGroup是一个不可以在第一次使用后复制的对象。而goroutine的主函数其实是传值的方法传递了WaitGroup。这里可以特别注意下i的输出是符合预期的。
好,让我们接下来看第二段代码:
package main
import (
"log"
"sync"
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
log.Printf("i:%d", i)
wg.Done()
}()
}
wg.Wait()
log.Println("exit")
}
输出:
go run wgtest2.go
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 i:5
2017/01/01 23:48:10 exit
没有死锁,但是i值的输出是错误的。因为,Go语言里面upvalue是引用的。Goroutine多次捕获的是同一个i。
再来,我们看第三段代码:
package main
import (
"log"
"sync"
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
log.Printf("i:%d", i)
wg.Done()
}()
}
wg.Wait()
log.Println("exit")
}
输出:
go run wgtest3.go
2017/01/01 23:51:46 i:5
2017/01/01 23:51:46 i:5
2017/01/01 23:51:46 i:5
2017/01/01 23:51:46 i:4
2017/01/01 23:51:46 i:5
2017/01/01 23:51:46 exit
没死锁,i的数值还是不对。因为upvaule的i是byRef传递。注意,这里出现了4个5和一个4,最终输出什么其实是随机,取决于操作系统和硬件。goroutine调度的越快,就越可能出现比5小的输出。
再来,我们看第四段代码:
package main
import (
"log"
"sync"
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup, i int) {
log.Printf("i:%d", i)
wg.Done()
}(&wg, i)
}
wg.Wait()
log.Println("exit")
}
输出:
go run wgtest4.go
2017/01/01 23:56:51 i:1
2017/01/01 23:56:51 i:0
2017/01/01 23:56:51 i:4
2017/01/01 23:56:51 i:2
2017/01/01 23:56:51 i:3
2017/01/01 23:56:51 exit
一切正常,符合预期。但是,这种写法却比较累赘。首先,没有利用闭包的upvalue来构建一个高阶函数,而是恢复到传统的传值,同时这种写法对写代码的人的心智负担太重了,传值和传引用要手动指定,而且还要在goroutine的主函数入口一一指定。那么我们推荐的写法应该是什么样子的呢?
最后,来看第五段代码:
package main
import (
"log"
"sync"
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
func(i int) {
wg.Add(1)
go func() {
log.Printf("i:%d", i)
wg.Done()
}()
}(i)
}
wg.Wait()
log.Println("exit")
}
输出:
go run wgtest5.go
2017/01/02 00:03:32 i:4
2017/01/02 00:03:32 i:0
2017/01/02 00:03:32 i:1
2017/01/02 00:03:32 i:2
2017/01/02 00:03:32 i:3
2017/01/02 00:03:32 exit
一样的一切正常。但是在第五段代码中,Goroutine的主函数是没有参数的。传引用的情况利用了upvalue,而需要传值的i变量用了一个外包函数的参数来复制。因为每次循环都会调用这个外包函数,从而复制了一次i的数值,虽然里层的Goroutine主函数还是 通过 upvalue来捕获i,不过每次捕获的都是外包函数的i副本而已。
综上所述,处于降低开发人员心智负担的考虑,我建议:
1. Go语言里面的goroutine的入口函数不要传递参数。
2. 所有的传ref参数都通过upvalue来捕获。
3. 如果要传值,可以在goroutine外面包一个函数,把要传value的参数用传值的方法传给这个外包的函数。参数名保持同名。