Go中Range方法你整明白了吗?
先看个例子
请说出下面函数的输出值分别是什么?
func listing1() {
a := [3]int{0, 1, 2}
for i, v := range a {
a[2] = 10
if i == 2 {
fmt.Println(v)
}
}
}
func listing2() {
a := [3]int{0, 1, 2}
for i := range a {
a[2] = 10
if i == 2 {
fmt.Println(a[2])
}
}
}
func listing3() {
a := [3]int{0, 1, 2}
for i, v := range &a {
a[2] = 10
if i == 2 {
fmt.Println(v)
}
}
}
结果分别是 2
, 10
, 10
。 第一个函数 listing1
不容易理解。 我们就来深入了解下 range
方法的用法。
range
Go 语言中 range 关键字用于 for 循环中迭代数组(array
)、切片(slice
)、通道(channel
)或集合(map
)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value
对。
可以速记为
msc
, 这里我们再次列出来之前的速记,我们复习一下。
- 可以与
nil
进行比较的是:mscfip
- 可以使用
make
进行初始化的是:msc
- 可以使用range遍历的是:
msc
range for slice
注释解释了遍历slice的过程: 下面是源码中的代码
// The loop we generate:
// for_temp := range
// len_temp := len(for_temp)
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = for_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
遍历 slice
前会先获取 slice
的长度 len_temp
来作为循环次数,循环体中,每次循环会先获取元素值,如果 for-range
中接收 index
和 value
的话,则会对 index
和 value
进行一次赋值。数组与数组指针的遍历过程与 slice
基本一致。
由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是无法遍历到的。
可以理解为range
在编译阶段已经对长度和里面的值进行了拷贝复制出来。
循环永动机
如果我们在遍历数组的同时修改数组的元素,能否得到一个永远都不会停止的循环呢?你可以尝试运行下面的代码:
func main() {
arr := []int{1, 2, 3}
for _, v := range arr {
arr = append(arr, v)
}
fmt.Println(arr)
}
其输出结果为: 1 2 3 1 2 3
上述代码的输出意味着循环只遍历了原始切片中的三个元素,我们在遍历切片时追加的元素不会增加循环的执行次数,所以循环最终还是停了下来。
range for map
// The loop we generate:
// var hiter map_iteration_struct
// for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
// index_temp = *hiter.key
// value_temp = *hiter.val
// index = index_temp
// value = value_temp
// original body
// }
遍历 map
时没有指定循环次数,循环体与遍历 slice
类似。由于 map
底层实现与 slice
不同,map 底层使用 hash
表实现的。
插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。
range for channel
// The loop we generate:
// for {
// index_temp, ok_temp = <-range
// if !ok_temp {
// break
// }
// index = index_temp
// original body
// }
channel
遍历是依次从 channel
中读取数据,读取前是不知道里面有多少个元素的。如果 channel
中没有元素,则会阻塞等待,如果 channel
已被关闭,则会解除阻塞并退出循环。
注意:
- 上述注释中
index_temp
实际上描述是有误的,应该为value_temp
,因为index
对于channel
是没有意义的。 - 使用
for-range
遍历channel
时只能获取一个返回值。
Go 1.22 对for循环进行了两个大更新
Go 1.22 版本于 2024 年 2 月 6 日正式向世界宣告了版本的发布 。此此版本也带来了一定for range的更新。
循环不再共享循环变量
Go1.22之前版本for 循环声明的变量只创建一次,并在每次迭代中进行更新,这会导致遍历时访问value时实际上都是访问的同一个地址的值。
func main() {
done := make(chan bool)
values := []string{"xiao", "xu", "code"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// 等待所有的 goroutine 执行结束
for _ = range values {
<-done
}
}
// 上述代码运行结果如下所示: code, code, code
这三个创建的 goroutine 都在打印同一个变量 v,所以它们通常会打印出 "code"、"code"、"code",而不是以某种顺序打印出 "xiao"、"xu" 和 "code"。
🚩 这就是共享循环变量造成的问题!
这个比较好理解,这个循环的 v 只创建一次,在每次循环的时候都会更新,而 闭包在访问 v 时实际上都访问的是同一个内存地址,所以最终打印的都是同一个值。
解决办法:
在Go版本不变的情况下,可以通过下面两种方式修改代码避免这个问题。
1:将for循环中传入v,代码改造如下
values := []string{"xiao", "xu", "code"}
for _, v := range values {
go func(v string) {
fmt.Println( v)
done <- true
}(v)
}
2:在循环中重新定义一个变量进行再次赋值
values := []string{"xiao", "xu", "code"}
for _, v := range values {
value := v
go func() {
fmt.Println( value)
done <- true
}()
}
Go1.22版本
不过这个问题在1.22版本已经得到处理了,大家用这个版本的时候可以放心使用了,太爽了吧!
我们在1.22版本上运行和1.21一样的代码
func main() {
done := make(chan bool)
values := []string{"xiao", "xu", "code"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// 等待所有的 goroutine 执行结束
for _ = range values {
<-done
}
}
// 上述代码运行结果如下所示:code, xiao, xu
for 循环的每次迭代都会创建新变量,每次循环迭代各自的变量,以避免意外共享错误。上面一模一样的代码,输出结果不再是固定的 code。
支持整数范围进行循环迭代
在 Go 1.22 版本之前, for range
仅支持对 array or slice
、string
、map
和 channel
类型的进行迭代。
而自 Go 1.22 版本起,新增了整数类型的迭代支持,我们能够直接使用整数进行循环迭代。
下面同样列举不同版本的例子,看看差异性!
Go1.22 之前版本
func main() {
for i := range 5 {
fmt.Println("小许code", i)
}
}
不支持遍历整数范围,这个range 5就直接提示报错了,编译当然有问题了
.\main.go:15:17: cannot range over 5 (untyped int constant)
Go1.22版本
func main() {
for i := range 5 {
fmt.Println("小许code", i)
}
}
巩固一下
func main() {
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
}
}
// 输出结果为 0,1,2
已关闭的 channel 仍然有数据
不能依据从通道中获得的第二个判断,通道是否关闭了。
func main() {
ch := make(chan int, 3)
ch <- 1
close(ch)
// 第一次从通道中获取数据
value, ok := <-ch
fmt.Println("第一次从通道中获取数据 value", value) // 1
fmt.Println("第一次从通道中获取数据 ok", ok) // true
// 第二次从通道中获取数据
value, ok = <-ch
fmt.Println("第二次从通道中获取数据 value", value) // 0
fmt.Println("第二次从通道中获取数据 ok", ok) // false
}
总结
- 遍历过程中可以适情况丢弃
index
或value
,可以一定程度上提升性能 - 遍历
channel
时,如果channel
中没有数据,可能会阻塞 - 使用
index
,value
接收range
返回值会发生一次数据拷贝 - 已经
for-range
在 go1.22中的版本升级