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

相关推荐
l_tian_tian_10 分钟前
JavaWeb——Ajax、Element、打包部署
前端·javascript·ajax
moskidi12 分钟前
Web day02 Js & Vue & Ajax
前端·javascript·vue.js
星月昭铭12 分钟前
浏览器控制台中使用ajax下载文件(没有postman等情况下)
前端·chrome·ajax·postman
花下的晚风14 分钟前
vue3 发送 axios 请求时没有接受到响应数据
前端·javascript·vue.js
盏灯26 分钟前
🏆🚀🎉前端开发,实现头像从圆圈⭕️伸出来的效果🕳
前端·css
2401_854391081 小时前
企业OA管理系统:Spring Boot技术架构与应用
spring boot·后端·架构
天农学子1 小时前
elementui el-input修改字体样式
前端·javascript·css·elementui
潜洋1 小时前
Spring Boot教程之七: Spring Boot –注释
java·spring boot·后端·注释
不能只会打代码1 小时前
深入讲解Spring Boot和Spring Cloud,外加图书管理系统实战!
spring boot·后端·spring cloud
一见1 小时前
go编程中yaml的inline应用
开发语言·后端·golang