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是一个数组,所以拷贝的对象是深拷贝,在迭代的时候修改a2的值不会影响原对象值。

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

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

解决方法

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

方法1,通过索引访问数组中的元素,具体代码如下。此时a2访问的是原始数组中的元素,所以输出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的地址,所以它们指向的数据是同一块内存。修改a2的值也会影响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对效率有明显提升作用。

相关推荐
AiMan6 分钟前
千问生成的内容能否下载吗?
前端
Anymous6 分钟前
我手写的 Redis 锁,输给了 10 秒的 TTL
后端·架构
北城笑笑7 分钟前
Vue 109 ,从零搭建 Vue3 + Electron 工程:生态、安全桥接与打包选型( Vue + Electron )
前端·javascript·vue.js·安全·electron·vue
JarvanMo21 分钟前
Flutter 3.50 & Dart 3.14发布,一起看看有哪些变化!
前端
莪_幻尘34 分钟前
4 个 3D 游戏,总体积 3.82MB:拆解 Claude 的 one-shot 工程链路
前端·ai编程·three.js
Highcharts.js43 分钟前
Highcharts智能数据分析平台Orbit 多语言配置技术文档
java·前端·javascript·highcharts·orbit·智能数据分析·设置文档
Rain的Java大神之路1 小时前
设计一套生产级 API 接口:从契约、安全、幂等到限流,我踩过的坑都在这了
java·后端·面试
SimonKing1 小时前
零配置也能白嫖前沿大模型!dsh-our-free-model插件真香警告
java·后端·程序员
nice_lcj5201 小时前
【从零理解 Spring AI · 15】让检索更准:先宽召回,再精排序
java·后端·rag·spring ai·ai应用开发·检索优化·重排序
回家路上绕了弯1 小时前
要人确认的那一步,在确认什么?
后端