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

使用指针元素切片或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] = &current
    }
}

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

相关推荐
yuchangchenTT9 分钟前
就是这个样的粗爆,手搓一个计算器:JSON格式化计算器
前端·json·365快速计算器·在线计算器
乐闻x13 分钟前
Vue Router 详细使用步骤:如何在 Vue 项目中配置 Vue Router
前端·javascript·vue.js
聚宝盆_15 分钟前
【css flex 多行均分有间隙布局】
前端·css
弗拉唐17 分钟前
SSM中maven
java·前端·maven
程序猿麦小七19 分钟前
基于springboot的社区团购管理系统的设计与实现
java·spring boot·后端·社区·团购
零希22 分钟前
CSS元素类型(二)
前端·javascript·css
潘多编程22 分钟前
Spring Boot观察者模式实战
spring boot·后端·观察者模式
煎饼果子呀25 分钟前
css-flex布局属性
开发语言·前端·css·html5
超栈28 分钟前
蓝桥杯-网络安全比赛题目-遗漏的压缩包
前端·网络·sql·安全·web安全·职场和发展·蓝桥杯
kevin_tech29 分钟前
Go API 多种响应的规范化处理和简化策略
开发语言·后端·golang·状态模式