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

相关推荐
小政爱学习!18 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。23 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼29 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093333 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
假装我不帅1 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax