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

相关推荐
李子琪。38 分钟前
网络空间安全深度实战:CSRF 漏洞原理剖析与基于 Token 的纵深防御体系构建(全栈实验报告)
前端·安全·csrf
冰暮流星1 小时前
javascript之history对象介绍
前端·笔记
IT_陈寒1 小时前
Vite热更新失灵?你可能漏了这个配置
前端·人工智能·后端
丷丩1 小时前
MapLibre GL JS第19课:实时更新要素
前端·javascript·gis·map·mapbox·maplibre gl js
Mr.Daozhi1 小时前
RAG 进阶实战:跑通 Demo 后我连续翻了 6 次车,逐一修复才真正可用(含 Gradio Web 版)
前端·数据库·langchain·大模型·gradio·rag·科研工具
uzong1 小时前
面试官:如何做好架构设计
后端·架构
哆来A梦没有口袋1 小时前
干货精讲 | 初级CSS面试高频考题
前端·css·面试
Cosolar2 小时前
QwenPaw Agent 实现原理深度剖析
后端·面试·架构
掘金012 小时前
EmbedPDF Vue 版 完整正文文档 全网首发
前端
Sincerelyplz2 小时前
【AI会议纪要实践】mapReduce、RAG 与结构化输出
java·后端·agent