使用指针元素切片或map的三种主要场景
当for range遍历的元素是指针时需要特别小心,否则可能会产生bug. 本文将详细分析该问题并给出解决方法。
在开始之前,让我们学习一下使用指针元素的切片或映射三种主要场景:
场景1
- 在语义方面,使用指针语义存储数据意味着共享数据。例如,下面的代码实现的是一个缓存功能,将元素缓存到map中。map的value为一个指针类型,表明Foo元素可以在Put和Store方法中共享。
golang
type Store struct {
m map[string]*Foo
}
func (s Store) Put(id string, foo *Foo) {
s.m[id] = foo
// ...
}
场景2
- 如果拿到的是对象的指针,直接存储指针到集合中相比存储值更方便。
场景3
- 如果操作的是大结构体,并且需要频繁修改,采用指针可以避免每次插入更新时的值拷贝的开销。像下面这段代码,存指针只需一步操作,而存值需要先从map中取值,更新值的内容,最后在存储到map中,需要多步操作。
golang
func updateMapValue(mapValue map[string]LargeStruct, id string) {
value := mapValue[id]
value.foo = "bar"
mapValue[id] = value
}
func updateMapPointer(mapPointer map[string]*LargeStruct, id string) {
mapPointer[id].foo = "bar"
}
问题引入
下面讨论 range 迭代的对象是指针类型存在的常见问题。定义Customer和Store两个结构体,Customer结构体包含ID和Balance两个字段,分别表示客户ID和资金额度信息,Store结构中包含一个map字段,map的值是一个指针,存储指向Customer对象的地址。
golang
type Customer struct {
ID string
Balance float64
}
type Store struct {
m map[string]*Customer
}
storeCustomers方法向Store中存入一批客户信息,入参是Customer切片,内部通过range迭代Customer切片,并且取customer的地址给s.m.
golang
func (s *Store) storeCustomers(customers []Customer) {
for _, customer := range customers {
s.m[customer.ID] = &customer
}
}
这段程序实际运行结果与我们预期的是一样的吗?跑一段如下的程序验证下。
golang
s.storeCustomers([]Customer{
{ID: "1", Balance: 10},
{ID: "2", Balance: -10},
{ID: "3", Balance: 0},
})
打印s.m的值,结果如下。啥?ID都是3,Balance都是0,为啥不是storeCustomers传入的内容呢?
console
key=1, value=&main.Customer{ID:"3", Balance:0}
key=2, value=&main.Customer{ID:"3", Balance:0}
key=3, value=&main.Customer{ID:"3", Balance:0}
原因分析
输出的值都是切片中第三个元素的内容,因为通过range迭代对象,不管对象中有多少个元素,创建的customer变量都是同一个,它的地址都是固定的。下面通过打印customer的内存地址验证这一结论。
golang
func (s *Store) storeCustomers(customers []Customer) {
for _, customer := range customers {
fmt.Printf("%p\n", &customer)
s.m[customer.ID] = &customer
}
}
运行上述程序,输出的地址为:
console
0xc00000c030
0xc00000c030
0xc00000c030
在第一次循环时,customer承载的是customers中第一个元素,然后将该customer的地址存储到map["1"]中,
在第二次循环时,customer承载的是customers中第二个元素,然后也将该customer的地址存储到map["2"]中,
在第三次循环时,customer承载的是customers中第三个元素,然后也将该customer的地址存储到map["3"]中。
当三次循环结束时,map["1"]、map["2"]和map["3"]中存储的地址是相同的,都是customer地址。但此时customer中装载的是第三个元素的数据,所以输出s.m中内容都是相同的。结合下面可以很容易理解整个处理过程。
解决方法
如何解决上述问题呢?主要有两种方法。
方法1是创建一个局部变量,将customer的值赋值给局部变量,map中存储局部变量的地址。因为每次循环时,分配的都是一个新的局部变量,互不干扰。
golang
func (s *Store) storeCustomers(customers []Customer) {
for _, customer := range customers {
current := customer
s.m[current.ID] = ¤t
}
}
方法2是使用索引遍历customers,具体代码如下。每次循环引用的地址是切片中的各个元素的地址,它们各不相同,所以s.m不会输出相同的内容。
golang
func (s *Store) storeCustomers(customers []Customer) {
for i := range customers {
customer := &customers[i]
s.m[customer.ID] = customer
}
}
总结
当我们使用for range 循环时要想到,每次迭代时左侧被赋值的变量都是同一个,所以如果赋值给它的是指针值,会导致所有引用该变量值的对象最后指向相同的地址。具体有两种解决办法,采用赋值局部变量或是通过索引循环。