坑一:迭代时协程引用索引和值

先看看下面的例子,你知道最终输出的结果是什么吗?

package main

import (
    "fmt"
    "time"
)

func main() {
    var m = []int{1, 3, 5}
    for i, v := range m {
        go func() {
            fmt.Println(i, v)
        }()
    }
    time.Sleep(time.Second)
}

不知道的同学,大家可以在 https://play.golang.org/ 这里尝试运行一下。

正确答案是:

2 5
2 5
2 5

这是为啥?三个 goroutine 都是输出了最后迭代的索引和值。

我觉得,理解清楚以下两点就可以了:

  1. 闭包内引用了外部变量 i 和 v,三个协程都引用了
  2. 协程运行时,循环可能已经结束了

要想解决这个问题,可以改成闭包传参的形式:

func main() {
    var m = []int{1, 3, 5}
    for i, v := range m {
        go func(_i, _v int) {
            fmt.Println(_i, _v)
        }(i, v)
    }
    time.Sleep(time.Second)
}

也可以让每一轮循环都用新的变量:

func main() {
    var m = []int{1, 3, 5}
    for i, v := range m {
        _i := i
        _v := v
        go func() {
            fmt.Println(_i, _v)
        }()
    }
    time.Sleep(time.Second)
}

运行结果就符合预期了:

0 1
1 3
2 5

坑二:迭代时值为原先迭代对象的拷贝

package main

import (
    "fmt"
)

func main() {
    var m = []int{1, 3, 5}
    for i, v := range m {
        if i == 1 {
            v = 2
        }
        fmt.Println(i, v)
    }
    fmt.Println(m)
}

这个输出是啥?

0 1
1 2 // 明明改成 2 了,
2 5
[1 3 5] // 这里还是 3 ?

那是因为,Go 会在 range 循环中自动为遍历的对象创造一个副本,可以理解为一个值拷贝,如果真的想修改原数组,你得这样写:

func main() {
    var m = []int{1, 3, 5}
    for i, v := range m {
        if i == 1 {
            m[i] = 2 // 改成 m[i]
        }
        fmt.Println(i, v)
    }
    fmt.Println(m)
}

程序输出是这样:

0 1
1 3 // 由于是值拷贝,所以改了原来的 m[i] 不影响 v 的值,m[i]=2,v=3.
2 5
[1 2 5] // 可以看到已经改成 2 了

也不算是什么大坑,理解了感觉和 PHP 的 foreach 语法差不多。

暂无评论