likes
comments
collection
share

Go语言中常见100问题-#32 for range作用于指针或map会产生啥问题?

作者站长头像
站长
· 阅读数 3

使用指针元素切片或map的三种主要场景

当for range遍历的元素是指针时需要特别小心,否则可能会产生bug. 本文将详细分析该问题并给出解决方法。

在开始之前,让我们学习一下使用指针元素的切片或映射三种主要场景:

场景1
  • 在语义方面,使用指针语义存储数据意味着共享数据。例如,下面的代码实现的是一个缓存功能,将元素缓存到map中。map的value为一个指针类型,表明Foo元素可以在Put和Store方法中共享。
type Store struct {
    m map[string]*Foo
}

func (s Store) Put(id string, foo *Foo) {
    s.m[id] = foo
// ...
}
场景2
  • 如果拿到的是对象的指针,直接存储指针到集合中相比存储值更方便。
场景3
  • 如果操作的是大结构体,并且需要频繁修改,采用指针可以避免每次插入更新时的值拷贝的开销。像下面这段代码,存指针只需一步操作,而存值需要先从map中取值,更新值的内容,最后在存储到map中,需要多步操作。

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对象的地址。

type Customer struct {
    ID string
    Balance float64
}

type Store struct {
    m map[string]*Customer
}

storeCustomers方法向Store中存入一批客户信息,入参是Customer切片,内部通过range迭代Customer切片,并且取customer的地址给s.m.

func (s *Store) storeCustomers(customers []Customer) {
    for _, customer := range customers {
        s.m[customer.ID] = &customer
    }
}

这段程序实际运行结果与我们预期的是一样的吗?跑一段如下的程序验证下。

s.storeCustomers([]Customer{
    {ID: "1", Balance: 10},
    {ID: "2", Balance: -10},
    {ID: "3", Balance: 0},
})

打印s.m的值,结果如下。啥?ID都是3,Balance都是0,为啥不是storeCustomers传入的内容呢?

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的内存地址验证这一结论。

func (s *Store) storeCustomers(customers []Customer) {
    for _, customer := range customers {
        fmt.Printf("%p\n", &customer)
        s.m[customer.ID] = &customer
    }
}

运行上述程序,输出的地址为:

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中内容都是相同的。结合下面可以很容易理解整个处理过程。

Go语言中常见100问题-#32 for range作用于指针或map会产生啥问题?

解决方法

如何解决上述问题呢?主要有两种方法。

方法1是创建一个局部变量,将customer的值赋值给局部变量,map中存储局部变量的地址。因为每次循环时,分配的都是一个新的局部变量,互不干扰。

func (s *Store) storeCustomers(customers []Customer) {
    for _, customer := range customers {
        current := customer
        s.m[current.ID] = &current
    }
}

方法2是使用索引遍历customers,具体代码如下。每次循环引用的地址是切片中的各个元素的地址,它们各不相同,所以s.m不会输出相同的内容。

func (s *Store) storeCustomers(customers []Customer) {
    for i := range customers {
        customer := &customers[i]
        s.m[customer.ID] = customer
    }
}

总结

当我们使用for range 循环时要想到,每次迭代时左侧被赋值的变量都是同一个,所以如果赋值给它的是指针值,会导致所有引用该变量值的对象最后指向相同的地址。具体有两种解决办法,采用赋值局部变量或是通过索引循环。