logo

循环

Last Updated: 2023-02-23

入门。

和大多数语言一样,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 里的循环变量(这里的kv)是基于 loop 的而不是基于 iteration 的,也就是说每个 iteration 里用的 k 和 v 都是同样的变量,只是值变了。所以这 4 次 iteration 产生的 goroutine 用的都是同样的kv,而当他们运行到打印的时候,kv已经到了循环的末尾,所以我们看到的都是数组最后一个值。

解决方法有两个,一种是把 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)
    }()
}

参考