前言
该篇紧接上篇文章【聊一聊go语言中的控制语句】https://blog.youkuaiyun.com/qq_41705360/article/details/143082692
对go语言控制语句不了解的同学,建议先看看上篇。虽然Go只有一种for语句形式,但可能遇到的“坑”却并不少,这里列出一些典型的“坑”,来看看你有没有踩中吧!
1.1 循环变量重用
看一下下面代码:
func main() {
var m = []int{1, 2, 3, 4, 5}
for i, v := range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
time.Sleep(time.Second * 10)
}
你预期的输出是什么呢?因为go协程运行的顺序不同,执行结果的顺序会有差别,但是结果是不是下面这样呢
0 1
4 5
1 2
2 3
3 4
实际输出是什么呢?执行一下,得到如下结果:
4 5
4 5
4 5
4 5
4 5
为什么会输出这个结果呢?我将上述代码做一个等价变换你就明白了:记住这个替换公式,要考的的哟,简称公式1
func main() {
var m = []int{1, 2, 3, 4, 5}
{
i, v := 0, 0
for i, v = range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
}
time.Sleep(time.Second * 10)
}
我们看到:i, v两个变量不是在每次循环时重新声明,而是在整个循环过程中只定义了一份,这就是为何所有goroutine输出的都是“4 5”的原因。
正确的写法:因为go语言参数的传递都是值传递,所以方式1其实类似与方式2,在传递给协程i,v参数的时候,会重新拷贝一份。
var m = []int{1, 2, 3, 4, 5}
方式1:
for i, v := range m {
go func(i, v int) {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}(i, v)
}
方式2:
for i, v := range m {
i := i
v := v
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
1.2 range表达式副本
例1:我们再来看一段代码:
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("original a =", a)
for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}
在你的预期中,上面程序的输出结果是这样的:
original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]
不过实际运行一下,你会看到真正的输出是这样的:
original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]
究其原因,是因为参数range循环的是a的副本,我们用a’来表示,将上面代码等价变换为下面后,就更容易理解了:也记住这个替换公式哟 简称公式2
for i, v := range a' { //a'是a的一个值拷贝
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
这样变换后,我们知道for range遍历的是a的副本,对a的修改不会影响后续的遍历。因此,当使用数组、切片作为range后的待遍历的容器集合时,要十分小心。
如果想要对a的修改,影响到r呢,聪明的你肯定想到了,那我们就去a的地址呗,这样即使a’是a的副本,但是因为拷贝的是地址,所以a’和a指向的是同一个底层数组。
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("original a =", a)
for i, v := range &a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
例2:再来看看下面这个例子
arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
res = append(res, &v)
}
//expect: 1 2
fmt.Println(*res[0],*res[1])
//but output: 2 2
通过查看go编译源码可以了解到, for-range其实是语法糖,内部调用还是for循环,初始化会拷贝带遍历的列表(如array,slice,map),然后每次遍历的v都是对同一个元素的遍历赋值。也就是说如果直接对v取地址,最终只会拿到一个地址,而对应的值就是最后遍历的那个元素所附给v的值。和上面的列子一样,我们使用公式1变形一下代码。上述代码等价与
arr := [2]int{1, 2}
res := []*int{}
{
v:=0
for _, v := range arr {
res = append(res, &v)
}
}
//expect: 1 2
fmt.Println(*res[0],*res[1])
//but output: 2 2
如何正确的表达呢?
//使用局部变量
for _, v := range arr {
//局部变量v替换了v,也可用别的局部变量名
v := v
res = append(res, &v)
}
//这种其实退化为for循环的简写
for k := range arr {
res = append(res, &arr[k])
}
例3:再来看看这个例子,应该输出什么呢?
v := []int{1, 2, 3}
for i := range v {
v = append(v, i)
}
fmt.Println(v)
咋一看,好像代码有问题了,边遍历边append增加元素值?这岂不是死循环了?我们使用公式2转换成下述形式
v := []int{1, 2, 3}
for i := range v' {
v = append(v, i)
}
fmt.Println(v)
需要注意的是对v的拷贝只在range中(可以替换成v’方便理解),下面对v的操作 v = append(v, i)还是原来声明的v。
总结:在for k := range arr中,其中k只有一份地址,所有操作,都是对同一地址的k,arr也是拷贝。当你想不明白的时候,可以通过公式1,公式2进行替换,就会豁然开朗啦。
arr:=[]int{1,2,3}
for i,k:=rang arr{
}
等价于
arr:=[]int{1,2,3}
{
i:=i
k:=k
for i,k:=rang arr'{
}
}
例4:通过这么多例子,讲到了rang 的拷贝,那在来看看这个例子
//假设值都为1,这里只赋值3个
var arr = [102400]int{1, 1, 1}
for i, n := range arr {
//just ignore i and n for simplify the example
_ = i
_ = n
}
这种方式有什么问题呢?因为arr是一个大的数组,遍历前的拷贝对内存是极大浪费。怎么优化?有两种,核心思想是只拷贝地址
1.对数组取地址遍历
for i, n := range &arr
2.对数组做切片引用
for i, n := range arr[:]
1.3对map的遍历?
对map的遍历时删除元素能遍历到么?
var m = map[int]int{1: 1, 2: 2, 3: 3}
//only del key once, and not del the current iteration key
var o sync.Once
for i := range m {
o.Do(func() {
for _, key := range []int{1, 2, 3} {
if key != i {
fmt.Printf("when iteration key %d, del key %d\n", i, key)
delete(m, key)
break
}
}
})
fmt.Printf("%d%d ", i, m[i])
}
答案是【不会】map内部实现是一个链式hash表,为保证每次无序,初始化时会随机一个遍历开始的位置,这样,如果删除的元素开始没被遍历到(上边once.Do函数内保证第一次执行时删除未遍历的一个元素),那就后边就不会出现。
对map遍历时新增元素能遍历到么?
var m = map[int]int{1:1, 2:2, 3:3}
for i, _ := range m {
m[4] = 4
fmt.Printf("%d%d ", i, m[i])
}
答案是【可能会】,输出中可能会有44。原因同上一个, 可以用以下代码验证。
var createElemDuringIterMap = func() {
var m = map[int]int{1: 1, 2: 2, 3: 3}
for i := range m {
m[4] = 4
fmt.Printf("%d%d ", i, m[i])
}
}
for i := 0; i < 50; i++ {
//some line will not show 44, some line will
createElemDuringIterMap()
fmt.Println()
}
- 遍历到新增的元素: 有时候您可能会遍历到新增的元素,这是因为在遍历的过程中,某个键的哈希值对应的桶位置发生了变化,使得该键处于当前遍历的范围内。
- 遍历不到新增的元素: 有时候您可能会遍历不到新增的元素,这是因为在遍历的过程中,新增的元素的哈希值可能对应的桶位置不在当前遍历的范围内,或者新增的元素在遍历之前就被插入了,遍历过程中并未遍历到它。
1.4 break未跳出for
当for与switch语句联合使用时,也要注意避坑,看一下下面代码:
func main() {
var sl = []int{5, 19, 6, 3, 8, 12}
var firstEven int = -1
// find first even number of the interger slice
for i := 0; i < len(sl); i++ {
switch sl[i] % 2 {
case 0:
firstEven = sl[i]
break
case 1:
// do nothing
}
}
println(firstEven)
}
执行这个代码,输出结果为12,与我们预期的第一个偶数6不符。原因是什么呢?从输出结果为12来看,应该是break并未跳出for循环,导致循环继续进行到最后。
记住:Go语言规范中明确规定,不带label的break语句中断执行并跳出的,是同一函数内break语句所在的最内层的for、switch或select。所以,上面这个例子的break语句实际上只跳出了switch语句,并没有跳出外层的for循环,这也就是程序未按我们预期执行的原因。
总结
以上通过大量例子例举了for range中常见的坑,其实归根揭底只要我们牢记公式1与公式2,很多问题就疑难而解啦。谢谢您的阅读,欢迎点赞,收藏,评论,关注哟!后续将持续输出编码世界。