循环
入门。
和大多数语言一样,Go 提供 for 循环,注意如果循环访问一个数组,它有两个返回值,一个是 index,或 key,一个是 value。
for k, v := range []int{1, 3, 5, 7} {
fmt.Printf("key = %v, value = %v\n", k, v)
}
key = 0, value = 1
key = 1, value = 3
key = 2, value = 5
key = 3, value = 7
放弃?
学 Go 怎么能不学 goroutine?我们来跑一个同样的循环,但把打印的工作放到 goroutine 里。这里需要一个WaitGroup
来等待。
// 创建一个WaitGroup来跑多个goroutines
var wg sync.WaitGroup
for k, v := range []int{1, 3, 5, 7} {
// 增加WaitGroup的计数
wg.Add(1)
go func() {
// 结束的时候别忘了通知WaitGroup
defer wg.Done()
// 假装很忙
time.Sleep(time.Second)
fmt.Printf("key = %v, value = %v\n", k, v)
}()
}
wg.Wait()
结果是打印出来的全是最后一个值:
key = 3, value = 7
key = 3, value = 7
key = 3, value = 7
key = 3, value = 7
难道是WaitGroup
的问题?我们也可以用 channel,至少数够 4 个done
就算完事。
done := make(chan bool)
values := []int{1, 3, 5, 7}
for k, v := range values {
go func() {
time.Sleep(time.Second)
fmt.Printf("key = %v, value = %v\n", k, v)
done <- true
}()
}
for range values{
<-done
}
得到的也是一样的结果。
key = 3, value = 7
key = 3, value = 7
key = 3, value = 7
key = 3, value = 7
进阶!
我们说的“循环”loop 是包含了很多次的“迭代”iteration,换句话说整个for
的那一坨(从for
到}
)是一个 loop,而 for 里面的代码每次执行是一次 iteration。Go 里的循环变量(这里的k
和v
)是基于 loop 的而不是基于 iteration 的,也就是说每个 iteration 里用的 k 和 v 都是同样的变量,只是值变了。所以这 4 次 iteration 产生的 goroutine 用的都是同样的k
和v
,而当他们运行到打印的时候,k
和v
已经到了循环的末尾,所以我们看到的都是数组最后一个值。
解决方法有两个,一种是把 k 和 v 当成参数传给 goroutine,这样打印出来的 k 和 v 就是生成 goroutine 时传进来的值,而不再是循环变量:
for k, v := range []int{1, 3, 5, 7} {
wg.Add(1)
go func(k, v int) {
defer wg.Done()
time.Sleep(time.Second)
fmt.Printf("key = %v, value = %v\n", k, v)
}(k, v)
}
另一个方法是保持 goroutine 不动,但另创建两个本地变量(k := k
, v := v
):
for k, v := range []int{1, 3, 5, 7} {
k := k
v := v
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(time.Second)
fmt.Printf("key = %v, value = %v\n", k, v)
}()
}
参考
- [GopherCon 2022] Compatibility: How Go Programs Keep Working https://www.youtube.com/watch?v=v24wrd3RwGo
- https://go.dev/doc/faq#closures_and_goroutines