原来Golang的Foreach这么坑!

开发 前端
用过golang的同学,相信对「for range」是再熟悉不过了,可以说在任何语言中,循环遍历都是常用的再也不能常用的一种方式,不过最近发现了一个问题,其实挺坑的,今天总结一下,希望对您有用.

前言

用过golang的同学,相信对「for range」是再熟悉不过了,可以说在任何语言中,循环遍历都是常用的再也不能常用的一种方式,不过最近发现了一个问题,其实挺坑的,今天总结一下,希望对您有用。

坑1

咱们废话不用多说,直接看例子。

现象

dataFromDb := []int{1,2,3} //从数据库取出来的数据
 
  var finalData []*int //目标数据
 
  for _,i := range dataFromDb{
     finalData = append(finalData, &i)
 }
 for _, final := range finalData{
  fmt.Println(*final)
 }

上面的例子很简单

  • 从数据库取出来数据 1,2,3,赋值给 dataFromDb。
  • 循环遍历dataFromDb赋值给最终的目标数据 finalData。
  • 循环输出目标数据finalData。

直观的感受,上面简直是一段简单的不能再简单的代码了,相信大家会脱口而出最后finalData的值是1,2,3,但是我们实际运行一下,结果输出的却是

~/Sites/test » go run main.go                                                                                  
3
3
3

结果输出的全部都是3,显然这与我们的认知是不符合的,但是为什么会这样呢?如果想弄清这个原理,首先我们得知道for range到底干了什么。

for range原理

要想了解一个函数的原理,最好的方式就是看源码,我们来看一看for range到底干了什么。

源码来自于 go 编译器的 「gc.walkrange」, 编译器对 for range 表达式的解析如下:

// a为原始slice

ha := a

hv1 := 0

// slice长度

hn := len(a)

v1 := 0

v2 := nil // for i,v := range 中的 v

for ; h1 < hn ; h1++ {

    tmp := ha[hv1]

    v1,v2 := hv1,tmp

}
  • 每一次for range,其实是先复制出来了一个副本ha,本质上循环的其实是副本。
  • for range中,go语言会额外创建一个新的 v2 变量存储切片中的元素,「循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝, 且循环中每次都使用的v2变量」。

回到问题

for _,i := range dataFromDb{
     finalData = append(finalData, &i)
 }

对于i来说,相当于 var i int,然后在循环的过程中 i=1,i=2,i=3 &i是指向i的地址,「所以&i是永远不会变的」。

  • 第一次循环 &i指向i,i的值是1。
  • 第二次循环 &i指向i,i的值变成2了,同时也把第一次循环的i的结果改成2了。
  • 第三次循环 &i指向i,i的值变成3了,同时也把前两次循环的i的结果改成3了。

如何解决

其实解决办法很简单,引入「中间变量」即可,代码改成下面这个样子。

dataFromDb := []int{1,2,3}
 var finalData []*int
 for _,i := range dataFromDb{
  temp := i //引入中间变量,每一次循环都重新开辟了一个temp的空间
  finalData = append(finalData, &temp)
 }
 for _, final := range finalData{
  fmt.Println(*final)
 }

代码加入了「中间变量temp」temp:=i等价于。

var  temp int 
temp = 1
  • 第一次循环 temp开辟了一块空间,指向了i,temp的值为1。
  • 第二次循环 temp「重新开辟了一块空间」,指向了i,temp的值为2,因为是重新开辟的空间,所以不会影响到上一次循环。
  • 第三次循环 原理同上一步。

坑2

现象

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

大家可以想一想上面这段代码会输出什么

3
3
3

输出结果居然全部都是最后一个值,这是为什么呢?

原因

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

解决办法

解决办法其实比较简单,在闭包函数上增加参数,并且与go rountine绑定即可。

s := []int{1, 2, 3}
    for _, v := range s {
        go func(v int) {
            fmt.Println(v) // 输出结果3 1 2
        }(v)
    }
    select {}
责任编辑:姜华 来源: 程序员小饭
相关推荐

2019-03-14 09:29:02

Linux系统内存

2021-04-19 05:42:51

Mmap文件系统

2023-11-01 14:49:07

2020-09-24 06:44:54

HTTPS网站 HTTP

2022-12-06 17:30:04

2023-10-30 08:16:33

数据库插件Mybatis

2021-02-07 08:13:18

@DateTimeFo@NumberFormSpring

2022-10-21 08:17:13

MongoDB查询Document

2023-09-22 08:00:00

分布式锁Redis

2020-11-27 10:34:01

HTTPHTTPS模型

2021-04-26 10:24:52

Linux 开发操作系统

2021-04-19 07:35:01

Linuxhistory命令

2024-03-12 08:44:56

WebWorkerTypeScript语法

2014-10-08 15:00:50

SUSE操作系统云计算

2021-08-29 18:13:03

缓存失效数据

2019-03-15 10:55:12

通信系统手机

2017-02-22 14:09:31

Javaforeach反编译

2022-11-02 19:08:48

微服务轮询消费者

2023-07-26 00:32:33

注解抽象spring

2021-02-17 21:04:03

Ehcache缓存Java
点赞
收藏

51CTO技术栈公众号