Go语言中常见100问题-#31 for range 问题解密

前言

for range循环作用的对象需要是表达式,例如,for i,v := range exp. exp需要是表达式, 可以是字符串、数组、指向数组的指针、切片、map或者channel. 现在我们来讨论这样一个问题:表达式exp在什么时候求值的?搞清楚该问题,可以避免一些常识性问题。

1. for range 作用于切片

问题案例

下面的程序在遍历的过程中,会不断的向切片s中添加元素,循环会终止吗?

golang 复制代码
s := []int{0, 1, 2}
for range s {
    s = append(s, 10)
}
原因分析

搞清楚上述问题,需要知道当使用 range 循环时,表达式exp在开始循环之前只会进行一次求值。求值一次可以这么理解,表达式会被拷贝到一个临时变量中,后续迭代都是针对临时变量。上面的程序当s被求值时,会得到一个临时切片,如下图所示。

range循环时会向原切片s中添加元素,当循环3次结束时,切片s的状态如下。为啥循环3次就结束了,因为range的临时切片长度为3.

下面采用普通for循环的代码运行效果与上面的完全不同。这段代码是一个死循环,因为 len(s) 在每次迭代的时候都会重新计算切片中的元素个数,而每次循环时会向切片中添加元素,所以 len(s)的值在不断的增加,i的值始终小于len(s)。

golang 复制代码
s := []int{0, 1, 2}
for i := 0; i < len(s); i++ {
    s = append(s, 10)
}

上面通过具体的例子说明了range作用于切片产生的效果,下面来看看range作用于通道和数组时结果是什么样的。

2. for range作用于通道

问题案例

下面的程序创建了两个协程,分别向两个不同的通道中发送数据。在main协程中通过range操作从通道中取出数据,在迭代的时候将另一个通道ch2赋值给ch.

golang 复制代码
ch1 := make(chan int, 3)
go func() {
    ch1 <- 0
    ch1 <- 1
    ch1 <- 2
    close(ch1)
}()

ch2 := make(chan int, 3)
go func() {
    ch2 <- 10
    ch2 <- 11
    ch2 <- 12
    close(ch2)
}()

ch := ch1
for v := range ch {
    fmt.Println(v)
    ch = ch2
}
原因分析

前面已讨论了range作用exp求值问题,作用于通道也是相同的效果。开始ch被赋值为ch1,当range作用于ch时,会进行拷贝操作。所以无论后续在循环内部修改ch的值,都不会影响range中拷贝的临时对象,整个循环打印的是ch1中的值。输出内容如下:

console 复制代码
0
1
2

ch=ch2对for range迭代没有任何影响,如果在上面代码的最后加上 close(ch),将关闭的是通道ch2而不是ch1.

3. for range作用于数组

问题案例与原因分析

下面是range作用于数组的例子,猜猜下面这段代码输出的内容是多少?正确答案是 2。为啥呢?前面说过,在range循环前会对表达式求值,所以会对a进行拷贝,range作用的对象是拷贝后对象。由于a是一个数组,所以拷贝的对象是深拷贝,在迭代的时候修改a[2]的值不会影响原对象值。

golang 复制代码
a := [3]int{0, 1, 2}
for i, v := range a {
    a[2] = 10
    if i == 2 {
        fmt.Println(v)
    }
}

结合下面的图示可以更有更清楚的理解。

解决方法

如果真想打印出修改后的值,如何处理呢?有两种方法:

方法1,通过索引访问数组中的元素,具体代码如下。此时a[2]访问的是原始数组中的元素,所以输出2.

golang 复制代码
a := [3]int{0, 1, 2}
for i := range a {
    a[2] = 10
    if i == 2 {
        fmt.Println(a[2])
    }
}

方法2,使用数组指针,具体代码如下。range作用前拷贝的是a的地址,所以它们指向的数据是同一块内存。修改a[2]的值也会影响range中原对象的值,输出值也是2.

golang 复制代码
a := [3]int{0, 1, 2}
for i, v := range &a {
    a[2] = 10
    if i == 2 {
        fmt.Println(v)
    }
}

虽然方法1和方法2都是有效的,但是方法2不会拷贝原数组的值,在数组很大的时候,采用方法2对效率有明显提升作用。

相关推荐
小白学习日记40 分钟前
【复习】HTML常用标签<table>
前端·html
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
姜学迁1 小时前
Rust-枚举
开发语言·后端·rust
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
爱学习的小健2 小时前
MQTT--Java整合EMQX
后端
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
北极小狐2 小时前
Java vs JavaScript:类型系统的艺术 - 从 Object 到 any,从静态到动态
后端
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081352 小时前
前端之路-了解原型和原型链
前端
永远不打烊2 小时前
librtmp 原生API做直播推流
前端