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

相关推荐
程序员爱钓鱼1 分钟前
Go语言实战案例-创建模型并自动迁移
后端·google·go
javachen__7 分钟前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
阿珊和她的猫2 小时前
v-scale-scree: 根据屏幕尺寸缩放内容
开发语言·前端·javascript
uzong6 小时前
技术故障复盘模版
后端
GetcharZp6 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
加班是不可能的,除非双倍日工资6 小时前
css预编译器实现星空背景图
前端·css·vue3
桦说编程6 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研6 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi7 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip7 小时前
vite和webpack打包结构控制
前端·javascript