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对效率有明显提升作用。

相关推荐
uhakadotcom6 分钟前
Caddy Web服务器初体验:简洁高效的现代选择
前端·面试·github
前端菜鸟来报道8 分钟前
前端react 实现分段进度条
前端·javascript·react.js·进度条
喝醉的小喵20 分钟前
分布式环境下的主从数据同步
分布式·后端·mysql·etcd·共识算法·主从复制
花楸树32 分钟前
前端搭建 MCP Client(Web版)+ Server + Agent 实践
前端·人工智能
wuaro32 分钟前
RBAC权限控制具体实现
前端·javascript·vue
专业抄代码选手37 分钟前
【JS】instanceof 和 typeof 的使用
前端·javascript·面试
用户00798136209737 分钟前
6000 字+6 个案例:写给普通人的 MCP 入门指南
前端
用户876128290737442 分钟前
前端ai对话框架semi-design-vue
前端·人工智能
雷渊43 分钟前
深入分析mybatis中#{}和${}的区别
java·后端·面试
干就完了11 小时前
项目中遇到浏览器跨域前端和后端解决方案以及大概过程
前端