背景:
开发阶段发现某数据结构切片中,字段值都相等,经排查后发现某次对结构体实例切片进行for循环遍历,以修改字段数值时,出现问题。
示例代码:
# forTest 模拟遍历循环结构体过程
func forTest() {
// 定义函数内部结构体
type forStruct struct {
ID int
Name string
}
// 生成数据切片,并进行赋值操作
list := make([]forStruct, 0)
list = append(list, forStruct{1, "测试1"}, forStruct{2, "测试2"})
// 存放遍历后修改的结果,其中result是结构体指针类型
result := make([]*forStruct, 0)
// 开始遍历数据,并打印出相关结果
for i, v := range list {
// 修改前数据
fmt.Println(fmt.Sprintf("第%d行数据:%v", i, v))
// 将ID值+10
v.ID = v.ID + 10
// 修改后数据
fmt.Println(fmt.Sprintf("第%d行修改数据:%v", i, v))
// 临时变量m的值
fmt.Println(fmt.Sprintf("v临时变量的地址:%v", &v))
// 切片原变量的值
fmt.Println(fmt.Sprintf("切片原变量的地址:%v", &list[i]))
fmt.Println("------------------------------------------")
result = append(result, &v)
}
// 遍历返回的结果
for i, r := range result {
fmt.Println(fmt.Sprintf("第%d行数据为%v", i, r))
}
}
输出结果:
第0行数据:{1 测试1}
第0行修改数据:{11 测试1}
v临时变量的地址:&{11 测试1}
切片原变量的地址:&{1 测试1}
------------------------------------------
第1行数据:{2 测试2}
第1行修改数据:{12 测试2}
v临时变量的地址:&{12 测试2}
切片原变量的地址:&{2 测试2}
------------------------------------------
第0行数据为&{12 测试2}
第1行数据为&{12 测试2}
可以看到输出的最后两行都是相同的,这是什么原因呢?
原因
golang中的参数传递都是值传递!
在for循环遍历slice或者map时,生成了临时变量v,变量v在遍历过程中,是重复利用的。也就是说,无论是第几次遍历,始终都是将slice或者map中的数据复制到了v中,并通过操作v来实现内部变量。
在上述示例代码中,处理逻辑是将遍历结构体数据切片并修改字段后,存入到结构体指针切片中,在此过程中,变量v的内存地址始终不变,存入的均为临时变量的地址。等循环结束后,由于golang使用的是引用计数算法,因此v并不会被垃圾回收,存入的所有结构体数据均为最后一次遍历的数据。
解决方式:
- 原切片改用指针类型:数据相同是由于存入的是临时变量地址,因此只需要将原本的切片存入类型从结构体统一成结构体指针即可。在操作v的时候,实际会操作切片底层的数据,操作完成后再次存储指针即可,或者直接使用原本的切片。
- 结果切片改为结构体: 与存入指针相对,也可以存为结构体。在遍历过程中,会生成一个新的结构体对象,并将这个结构实例存入结果中。不过建议大数据结构体还是使用指针,以减少内存复制成本。
- 直接操作原切片: 上述两种方式都是原现的切片类型统一,如果仍需要结构体转换为指针类型,则可不操作临时变量v,直接操作原变量,并将原变量赋值到结果集。