原理
for...range是go中用于遍历集合数据的语法糖,与for的不同之处在于range关键字会将集合中每个元素的值返回。
假如我们有这样一个集合[]int,使用range遍历数据时,range首先判断集合中每个元素类型为int,因为int在内存中对应的字节大小是明确的,所以range会取出int对应的内存字节中的数据返回。
举例说明
下面例子中我们定义了一个数组,分别用for,for range打印内存地址,以区分for range的特殊之处
items := [3]int{1, 2}
log.Printf("items的内存地址:%p", &items)
for i := 0; i < len(items); i++ {
log.Printf("索引:%d,元素的值:%d,元素的地址:%p", i, items[i], &items[i])
}
for i, item := range items {
log.Printf("索引:%d,元素的值:%d,元素的地址:%p", i, item, &item)
}
# out
items的内存地址:0xc00000e1c8
#for循环输出:
索引:0,元素的值:1,元素的地址:0xc00000e1c8
索引:1,元素的值:2,元素的地址:0xc00000e1d0
索引:2,元素的值:0,元素的地址:0xc00000e1d8
#for range输出:
索引:0,元素的值:1,元素的地址:0xc0000125a0
索引:1,元素的值:2,元素的地址:0xc0000125a0
索引:2,元素的值:0,元素的地址:0xc0000125a0
上面例子中我们可以看到items的内存地址为0xc00000e1c8与for循环中第一个元素的内存地址是一致的,这一点说明了items[i]每次获取都是指定元素索引对应的内存地址。
而for range中,我们发现所有元素的内存地址都是一样的,为什么会出现这个情况?这正是range在中间搞得鬼,这说明for range中,每次循环到一个元素时,range只把元素内存中对应值(字节数据)赋给了这个0xc0000125a0地址对应的变量,类似于如下代码:
items := [3]int{1, 2}
tmp:=new(int)
for i := 0; i < len(items); i++ {
*tmp=items[i]
log.Printf("索引:%d,元素的值:%d,元素的地址:%p", i, tmp, tmp)
}
# out
索引:0,元素的值:1,元素的地址:0xc0000125a8
索引:1,元素的值:2,元素的地址:0xc0000125a8
索引:2,元素的值:0,元素的地址:0xc0000125a8
for range避坑
既然我们知道range每次都会取元素对应内存的值返回,也就是copy一份数据返回,那么当我们使用range遍历数据时,如果元素类型是值类型,应该避免直接修改元素,如下例子:
items := [3]int{}
log.Println(items)
for i, item := range items {
//元素的值实际没有被修改,这里修改的只是range创建的一个临时变量item的值
item = i
log.Println(item)
}
log.Println(items)
# out
[0 0 0]
0
1
2
[0 0 0]
正确的做法应该像下面这样
items := [3]int{}
log.Println(items)
for i := range items {
items[i]=i
}
log.Println(items)
# out
[0 0 0]
[0 1 2]