Go语言的循环只有一种,就是以for开头。不像其他语言有多种形式。Go语言的for循环有3种形式:
1. for init; condition; post { } // 类似c语言for循环 init可以是一个简短的变量声明,一个递增或赋值语句,或者是一个函数调用!
2. for condition { } // 类似c语言while
3. for {} // 类似c语言for(;;)
为什么说类似C语言呢,下图是Go的“家谱”
for range 的使用
for循环的range格式可以对 slice、map、数组、字符串等进行迭代循环。
for range 在使用中的坑
for range 虽然使用方便,但是一不注意就容易掉坑里,需要注意以下几个方面:
1. for range 取不到所有元素的地址
arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
res = append(res, &v)
}
fmt.Println(*res[0],*res[1]) / /expect: 1 2
以为会是 1 2 实际是 2 2。 下面就来分析一下:
for range 其实是一种语法糖,对应的go编译源码链接
为什么会这样呢?本质原因就是变量v一直是同一个,地址是一样的,你也可以试一下,直接对v取地址,最终都是相同的一个地址,所以后面取出来的值会把前面的值给覆盖掉了。v地址最后的值会是最后一个赋值给v的值,这里为2。
解决办法:
- 使用局部变量拷贝, 这样每次都对v1进行初始化赋值,其地址是不同的。
for _, v := range arr {
v1 := v
res = append(res, &v1)
}
- 直接使用索引获取原来的元素
for k := range arr {
res = append(res, &arr[k])
}
这让我想到一个相似的东西,java当中dfs使用到双层列表的时候,在把单层列表加入的时候都会使用这样的语句:res.add(new ArrayList<>(track)),也就是在add的时候需要做一层拷贝,否则到最后全都是空列表,因为dfs遍历完成以后,回到了根节点,成为了空列表。引用传递需要注意的一个地方。
2. for range 循环的时候追加元素,循环会否停止
v := []int{1, 2, 3}
for i := range v {
v = append(v, i)
}
会的!for range 是语法糖,数组进行for range遍历前会对v进行拷贝,用拷贝的长度进行遍历,期间对原来v的修改不会显示到遍历中。修改会追加到原切片中,伪代码如下
// len_temp := len(range)
// range_temp := range
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = range_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
3. 大数组遍历的问题
var arr = [204800]int{1, 1, 1}
for i, n := range arr { _ = i
_ = n
}
在使用for range的时候会对原数组进行一次拷贝,但对大数组进行拷贝会十分浪费内存,如何解决?
- 一种是取址遍历,
for i, n := range &arr,这时候拷贝的是地址,内存占用小很多 - 另一种是对数组做切片引用
for i, n := range ``arr[:]
同时对于大型数组,由于参与for range的是该数组的拷贝,那么使用for range是不是会比经典for loop更耗资源且性能更差?结论是在编译器优化的情况下,性能甚至会更好,而在没有优化的情况下,两种loop的性能都大幅下降,并且for range下降更多,性能显著不如经典for loop。同时使用结构体方面:无论是哪种结构体类型,经典for loop遍历的性能都是一样的,但for range的遍历性能却会随着结构体字段数量的增多而下降。具体测试说明细节可以看看这篇文章:传送门
4. 对map删除元素是否能遍历到
var m = map[int]int{1: 1, 2: 2, 3: 3} //only del key once, and not del the current iteration key var once sync.Once
for i := range m {
once.Do(func() {
for _, key := range []int{1, 2, 3} {
if key != i {
fmt.Printf("当前 i:%d, del key:%d\n", i, key)
delete(m, key)
break
}
}
})
fmt.Printf("%d%d\n", i, m[i])
}
map删除的元素后面不会被访问到了,这里once的作用主要保证了map会有一个键值对被del,后面print只会print两个键值对。注意的是once里面的函数是一个for循环,会把1,2,3都取一次,直到取出来的和map随机取出来的不一样,也就是key != i
5. 对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])
}
可能会。map的for range是随机化的,具有不确定性,一种是遍历顺序的不确定性,还有一种是遍历范围的不确定性。
6. for range里面使用go routine
var m = []int{1, 2, 3}
for i := range m {
go func() {
fmt.Print(i)
}()
}
//阻塞1分钟等待所有goroutine运行完 time.Sleep(time.Millisecond)
以为的结果会是0,1,2;实际的结果是2,2,2。很有可能当for循环执行完之后,goroutine才开始执行,这个时候val的值是指向了切片中最后一个元素。可以通过参数方式传入或者使用局部变量拷贝。
for i := range m {
go func(i int) {
fmt.Print(i)
}(i)
}
for i := range m {
j := i
go func() {
fmt.Print(j)
}()
}