Golang for range 和闭包的坑

shensi · 2022-06-08 14:40:58 · 2626 次点击 · 预计阅读时间 3 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2022-06-08 14:40:58 的文章,其中的信息可能已经有所发展或是发生改变。

一、for range 坑:

例子:将数组元素的地址存入到指针map中

上代码:

    arr := []int{1, 2, 3} // 普通数组

    m := make(map[int]*int) // 指针map
    for i, v := range arr {
               // fmt.Println(&v)    // 如果在这里打印v的内存地址的话,会发现3次的地址都是一样的
        m[i] = &v
    }

    for _, v := range m {
        fmt.Println(*v) // 输出数据 3 3 3
    }

可以看出输出结果,并非预期那样 1 2 3.

从输出的数据可以推测,好像每次都输出了最的原始3 。

原因是什么:

其实原因是循环变量的作用域的规则限制。在上面的程序中,v 在 for 循环引进的一个块作用域内进行声明。在循环里创建的所有函数变量共享相同的变量,就是一个可访问的存储位置,而不是固定的值。

假设 v 变量的地址在 0xc0000180c0  上, for 循环在迭代过程中,所有变量值都是在这地址上迭代的。因此存入map中的地址3次都是一样的 0xc0000180c0

如何解决

    arr := []int{1, 2, 3} // 普通数组

    m := make(map[int]*int) // 指针map
    for i, v := range arr {
        fmt.Println(&v)
        vTmp := v    // 修改点
        m[i] = &vTmp // 修改点
    }

    for _, v := range m {
        fmt.Println(*v) // 输出数据 3 3 3
    }

在上面的程序中,for循环语句引入了新的词法块,循环变量v在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以v为例,后续的迭代会不断更新v的值,当操作执行时,for循环已完成,v中存储的值等于最后一次迭代的值。这意味着,每次都是相同的目录。

通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。

二、for range 使用闭包 坑:

错误示例:

    s := []int{1, 2, 3}
    for _, v := range s {
        go func() {
            fmt.Println(v) // 输出结果3 3 3
        }()
    }
    select {}

正确使用

    s := []int{1, 2, 3}
    for _, v := range s {
        go func(v int) {
            fmt.Println(v) // 输出结果3 1 2
        }(v)
    }
    select {}

原因:

在没有将变量 v 的拷贝值传进匿名函数之前,只能获取最后一次循环的值,这是新手最容易遇到的坑。

三、 匿名函数列表:

错误示例:

    var slice []func()  // 匿名函数列表
    sli := []int{1, 2, 3, 4, 5}
    for _, v := range sli {
        fmt.Println(&v)
        slice = append(slice, func() {
            fmt.Println(v * v) // 直接打印结果
        })
    }

    for _, val := range slice {
        val()
    }

输出结果:

0xc0000b2008
0xc0000b2008
0xc0000b2008
0xc0000b2008
0xc0000b2008
25
25
25
25
25

修改后代码:

    var slice []func()  // 匿名函数列表
    sli := []int{1, 2, 3, 4, 5}
    for _, v := range sli {
        t := v
        fmt.Println(&t) // 内存次之不同
        slice = append(slice, func() {
            fmt.Println(t * t) // 直接打印结果
        })
    }

    for _, val := range slice {
        val()
    }

输出结果:

0xc0000b2008
0xc0000b2020
0xc0000b2028
0xc0000b2030
0xc0000b2038
1
4
9
16
25

原因:

每次 append 操作仅将匿名函数放入到列表中,但并未执行,并且引用的变量都是 v,随着 v 的改变匿名函数中的 v 也在改变,所以当执行这些函数时,他们读取的都是环境变量 v 最后一次的值。解决的方法就是每次复制变量 v 然后传到匿名函数中,让闭包的环境变量不相同。

四、defer调用闭包

package main

import "fmt"

func main() {
    x, y := 1, 2

    defer func(a int) { 
        fmt.Printf("x:%d,y:%d\n", a, y)  // y 为闭包引用
    }(x)      // 复制 x 的值

    x += 100
    y += 100
    fmt.Println(x, y)
}

输出结果:

101 102
x:1,y:102

这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。

x和y的值代表了复制与引用的区别,什么时候使用复制什么时候使用引用需要谨慎考虑。

参考文档:

简书-田飞雨 简书-gurlan


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2626 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传