golang中,常用的遍历方式有经典的for循环和for range两种。实际上,使用 for range 语法的控制结构最终应该也会被golang的编译器转换成普通的 for 循环,所以for range实际上会被先转换成经典的for循环再真正执行,而正是这个转换过程常常会留坑。
下面简单表示这个转换过程:
for range代码是:
for index, value := range t_slice {
original body
}
转换后:
len_temp := len(t_slice)
range_temp := t_slice
for index_temp = 0; index_temp < len_temp; index_temp++ {
value_temp := range_temp[index_temp]
index := index_temp
value := value_temp
original body
}
实际编码中可能遇到的问题
循环会不会停?
代码如下:
package main
import "fmt"
func main() {
arr := []int{1,2}
for _, v := range arr {
arr = append(arr, v)
}
fmt.Println(arr)
}
结果:
[1 2 1 2]
- 1
从上面的转换过程可以看到,循环在还没开始时已经拿到了循环的长度,在循环的过程中不会改变。
遍历取所有元素地址?
代码如下:
package main
import "fmt"
func main() {
arr := []int{1,2}
res := []*int{}
for _, v := range arr {
res = append(res, &v)
}
fmt.Println(*res[0], *res[1])
}
结果:
2 2
由上面的转换过程可以看出,&v实际上是对循环内部同一个短变量的取址,因此res中存的其实都是同一个地址,这个地址中的值实际上是最后一次循环赋的值。
那若要取所有元素地址怎么做呢?
第一种方式:
package main
import "fmt"
func main() {
arr := []int{1,2}
res := []*int{}
for _, v := range arr {
v := v
res = append(res, &v)
}
fmt.Println(*res[0], *res[1])
}
实际上是在original body中引入一个新短变量替换原value;
第二种方式:
package main
import "fmt"
func main() {
arr := []int{1,2}
res := []*int{}
for i := range arr {
res = append(res, &arr[i])
}
fmt.Println(*res[0], *res[1])
}
实际上就是for循环;
这两种方式得到的结果:
1 2
在遍历中起协程?
代码如下:
package main
import (
"fmt"
"sync"
)
func main() {
var m = []int{1, 2, 3}
var wg sync.WaitGroup
for i := range m {
wg.Add(1)
go func() {
fmt.Print(i)
wg.Done()
}()
}
wg.Wait()
}
输出结果:
222
上面的代码中,每轮循环中使用匿名函数起协程,匿名函数直接引用外部变量i,实际上,这就是golang中的闭包,闭包是匿名函数与匿名函数所引用环境的组合,匿名函数有动态创建的特性,这使得匿名函数不用通过参数传递的方式,就可以直接引用外部的变量。
每轮循环启动一个协程,而协程启动与循环变量递增不是在同一个协程,协程启动的速度远小于循环执行的速度,所以即使是第一个协程刚起启动时,循环变量可能已经递增完毕。由于所有的协程共享循环变量i,而且这个i会在最后一个使用它的协程结束后被销毁,所以最后的输出结果都是循环变量的末值即2。
package main
import (
"fmt"
"time"
"sync"
)
func main() {
var wg sync.WaitGroup
var m = []int{1, 2, 3}
for i := range m {
wg.Add(1)
go func() {
fmt.Print(i)
wg.Done()
}()
time.Sleep(time.Second)
}
wg.Wait()
}
这样写,使得循环变量递增时间间隔增大到1s,足够协程启动,此时输出结果:
012
怎样更科学的解决这个问题呢?实际上跟前面类似,有以下两种方式:
第一种:
以参数方式传入:
package main
import (
"fmt"
"sync"
)
func main() {
var m = []int{1, 2, 3}
var wg sync.WaitGroup
for i := range m {
wg.Add(1)
go func(i int) {
fmt.Print(i)
wg.Done()
}(i)
}
wg.Wait()
}
第二种:
使用局部变量拷贝:
package main
import (
"fmt"
"sync"
)
func main() {
var m = []int{1, 2, 3}
var wg sync.WaitGroup
for i := range m {
wg.Add(1)
i := i
go func() {
fmt.Print(i)
wg.Done()
}()
}
wg.Wait()
}
注:
-
对于大数组,如果使用for range遍历,遍历前的转换过程会很浪费内存,可以优化:
(1)对数组取地址遍历for i, n := range &arr;(2)对数组做切片引用for i, n := range arr[:]; -
对于大数组的遍历重置为默认值,golang底层有优化,因此效率很高;
-
map遍历时删除元素,如果删除的元素开始没被遍历到,之后就不会出现;
-
map遍历时新增元素,可能会被遍历到;