从一道简单的例子开始
package main
import (
"fmt"
)
func main() {
dataFromDb := []int{1, 2, 3} //从数据库取出来的数据
var finalData []*int //目标数据
for _, v := range dataFromDb {
finalData = append(finalData, &v)
}
for _, final := range finalData {
fmt.Println(*final)
}
}
相信大家会脱口而出最后finalData
的值是 1,2,3,但是我们实际运行一下,结果输出的却是:
3
3
3
为什么会这样呢?如果想弄清这个原理,首先我们得知道for range
到底干了什么
for-range 原理
想了解一个函数的原理,最好的方式就是看源码,编译器对 for range
表达式的解析如下
// a为原始slice
ha := a // 拷贝原有的slice结构。意味着len是不变的,在循环中,不受后续slice长度变化的影响
h1 := 0
// slice长度
hn := len(a)
v1 := 0 // for i,v := range 中的 i
v2 := nil // for i,v := range 中的 v
for ; h1 < hn ; h1++ {
tmp := ha[h1]
v1,v2 = h1,tmp // ----- 赋值到 v1,v2 变量 ----
// ... 返回到外部执行代码中...
// ...跳回该底层代码处,继续执行该for循环...
}
for k,v := range slice
中:
- 会拷贝原有的
slice
结构,意味着len
是不变的。在循环中,不受后续slice
长度变化的影响。 go
语言会额外创建一个新的v2
变量存储切片中的元素值。
注意!不但整个原有 slice
结构会被拷贝,就连切片内的元素是会被拷贝出来,然后赋值给 v2
。
v2 由始至终是一个 for
外的变量对象,而不是 for
内的局部变量。前者无论 for 循环多少次,都是一个变量对象(占用同一个内存地址);后者会随着 for 循环的变化产生新的变量对象(不停创建局部变量,内存地址不一样)
关键点:
- slice 整个结构会被拷贝。意味着循环中的长度 len 是不变的
- slice 中的元素值会被拷贝到 v 中。意味着 v 的改变不会影响原有元素值
- range 底层的 v 自始至终都是同一个变量对象。意味着 v 的内存地址一直不变。
无论是数组、切片还是映射(map),for-range
都会对其进行拷贝,然后依次迭代每个元素!
分析
有了上面的理论基础作为支撑,那么我们就来进一步分析造成该现象结果的原因。
回到代码中
for _,v := range dataFromDb{
finalData = append(finalData, &v)
}
假设 v
变量地址 0x0030
, 那么 finalData
中三个元素的地址都为 0x0030
虽然每次循环都会把 dataFromDb
变量中的元素赋值到 v
中,v
被不断刷新着数据,但是 v 的地址是不变的,所以 finalData
最终都存储着相同的地址:0x0030
。
而这个地址最终将会指向 dataFromDb
的最后一个元素。
解决
其实解决办法很简单,引入**「中间变量」**即可,代码改成下面这个样子
dataFromDb := []int{1,2,3}
var finalData []*int
for _,v := range dataFromDb{
temp := v //引入中间变量,每一次循环都重新开辟了一个temp的空间
finalData = append(finalData, &temp)
}
for _, final := range finalData{
fmt.Println(*final)
}
代码加入了**「中间变量temp」**temp:=v等价于
var temp int
temp = 1
- 第一次循环 temp开辟了一块空间,指向了v,temp的值为1
- 第二次循环 temp**「重新开辟了一块空间」**,指向了v,temp的值为2,因为是重新开辟的空间,所以不会影响到上一次循环
- 第三次循环 原理同上一步
值拷贝问题
在for-range
循环中,Go
会对集合中的每个元素进行值拷贝(这个我们在前面的 for-range 源码分析也提到了)。
这意味着如果集合元素是大对象,拷贝操作可能会带来性能问题。来看一个例子:
data := []struct{ num int }{{1}, {2}, {3}}
for _, v := range data {
v.num++
}
在这里,每个v
都是data
元素的一个拷贝,而不是引用,因此对v.num
的修改不会影响data
本身。
要解决这个问题,你可以使用指针:
for i := range data {
data[i].num++
}
总结
更多 for 循环的坑,可以看我前面的文章。
希望这篇文章能帮助你更好地理解for-range
,在实际开发中写出更加健壮的代码。