代码改变世界

golang 循环变量

2022-04-09 11:55  youxin  阅读(162)  评论(0编辑  收藏  举报

下面例子:

package main

import "fmt"

func test1() {

    a1 := []int{1, 2, 3}
    a2 := make([]*int, len(a1))

    for i, v := range a1 {
        a2[i] = &v
    }
    fmt.Println("值:", *a2[0], *a2[1], *a2[2])

    fmt.Println("地址:", a2[0], a2[1], a2[2])
}
func main() {

    test1()
}

输出:

值: 3 3 3
地址: 0xc000016098 0xc000016098 0xc000016098
 
原因:,range在遍历值类型时,其中的v是一个局部变量,只会声明初始化一次,之后每次循环时重新赋值覆盖前面的,所以给a2[i]赋值的时候其实都是同一个地址&v,而v最终的值为a1最后一个元素的值,也就是3。
正确做法:
a2[i]赋值时传递原始指针,即a2[i] = &a1[i]
②创建临时变量t := va2[i] = &t
③闭包(与②原理一样),func(v int) { a2[i] = &v }(v)
 
更为隐秘的:
func main() {

    var out [][]int

    for _, i := range [][1]int{{1}, {2}, {3}} {

        out = append(out, i[:])

    }

    fmt.Println("Values:", out)}// 输出结果// [[3] [3] [3]]


原理也是一样的,不论遍历多少次,i[:]总是被本次遍历的值所覆盖

 

场景二,在循环体内使用goroutines

func main() {

    values := []int{1, 2, 3}

    wg := sync.WaitGroup{}

    for _, val := range values {

        wg.Add(1)

        go func() {

            fmt.Println(val)

            wg.Done()

        }()

    }

    wg.Wait()}// 输出结果// 3// 3// 3

 

分析

对于主协程来讲,循环是很快就跑完的,而这个时候各个协程可能才开始跑,此时val的值已经遍历到最后一个了,所以各协程都输出了3。(如果遍历数据庞大,主协程遍历耗时较久的话,goroutine的输出会根据当时候的val的值,所以每次的输出结果不一定相同的。)

解决办法
①使用临时变量


for _, val := range values {

    wg.Add(1)

    val := val    go func() {

        fmt.Println(val)

        wg.Done()

    }()}

②使用闭包


for _, val := range values {

    wg.Add(1)

    go func(val int) {

        fmt.Println(val)

        wg.Done()

    }(val)}

 

 

上面的错误代码vscode插件会提示:loop variable xx  captured by func literal loopclosure

 

协程引用循环变量的坑
循环体中启动协程异步执行,这个时候就容易出现问题了,比如下面这样一段代码就会出现我们不期望的结果。

for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
} ()
}
我们期望他能乱序输出0到9这几个数,但是他的执行结果并非如此。实际的执行结果如下:

7
10
10
10
10
10
10
10
10
7
可以看到他的执行结果大家基本都输出10。其实原因也很容易解释:

主协程的循环很快就跑完了,而各个协程才开始跑,此时i的值已经是10了,所以各协程都输出了10。(输出7的两个协程,在开始输出的时候主协程的i值刚好是7,这个结果每次运行输出都不一样)

 
 

参考:https://www.cnblogs.com/wscsq789/p/15388804.html