GoMap中结构体存储:值与指针区别解析
Go语言中,当结构体作为Map的值类型时,`map[int]struct`(值类型)与`map[int]*struct`(指针类型)存在显著差异。值类型存储结构体的副本,修改副本不影响原Map中的数据,但无法直接修改Map中的结构体成员,需取出修改后再存入。指针类型则存储结构体的引用,允许直接修改Map中的结构体成员,实现数据共享。选择哪种方式取决于对结构体可变性、内存管理和并发安全的需求。若需直接修改结构体或结构体较大,指针类型更优;若结构体较小且修改频率低,值类型可能更合适。理解Go Map元素的地址性限制是关键,Map元素不可直接寻址,因此直接修改`map[int]struct`中的结构体成员会导致编译错误。
核心区别:值语义与指针语义
在Go语言中,map[int]vertex 和 map[int]*vertex 在存储结构体时,体现了值语义(Value Semantics)和指针语义(Pointer Semantics)的根本差异。
map[int]vertex (值类型): 当你向Map中添加一个 vertex 结构体时,Go会创建一个该结构体的完整副本并将其存储在Map内部。这意味着Map中存储的每个元素都是一个独立的 vertex 实例。后续对原始结构体(或Map中取出的副本)的修改,不会影响Map中存储的那个副本,反之亦然。
map[int]*vertex (指针类型):* 当你向Map中添加一个 `vertex` 指针时,Map中存储的不是结构体本身,而是指向该结构体在内存中位置的一个引用**。因此,通过这个指针,你可以直接访问并修改原始结构体,且所有持有该指针的地方都会看到这些修改。这实现了数据共享。
以下代码示例清晰地展示了这一区别:
package main import "fmt" type vertex struct { x, y int } func main() { a := make(map[int]vertex) // 存储 vertex 值 b := make(map[int]*vertex) // 存储 *vertex 指针 v := &vertex{0, 0} // 创建一个 vertex 实例的指针 a[0] = *v // 将 v 指向的 vertex 值复制一份存入 map a b[0] = v // 将 v 指针本身存入 map b // 此时,v 和 b[0] 指向同一个内存地址 // a[0] 是 v 指向的 vertex 的一个独立副本 v.x, v.y = 4, 4 // 修改原始 v 指向的结构体 // 打印结果: // a[0].x, a[0].y 仍然是 0 0,因为它是一个副本,不受 v 修改的影响。 // b[0].x, b[0].y 变为 4 4,因为它是一个指针,指向的内存地址与 v 相同。 fmt.Println(a[0].x, a[0].y, b[0].x, b[0].y) // Output: 0 0 4 4 }
从输出可以看出,对原始 v 的修改只影响了 b[0],而 a[0] 保持不变,这正是值拷贝和引用传递的本质区别。
可变性与地址性问题
理解 map[int]vertex 与 map[int]*vertex 之间差异的关键在于Go语言中Map元素的地址性(Addressability)。在Go中,Map中的元素是不可寻址的(non-addressable)。这意味着你不能直接获取Map中某个值的内存地址,也就不能直接修改其内部字段。
当你尝试对 map[int]vertex 中的结构体成员进行直接修改时,例如 a[0].x = 3,编译器会报错 cannot assign to (a[0]).x。这是因为 a[0] 返回的是 vertex 结构体的一个副本,而不是其在Map中存储位置的引用。你不能直接修改这个副本,因为Map的设计不允许你直接修改其内部存储的值。如果允许这样做,当Map内部进行哈希表重排或扩容时,该副本的内存地址可能会改变,导致之前获取的地址失效,引发数据不一致或运行时错误。
相反,对于 map[int]*vertex,b[0] 返回的是一个 *vertex 指针。这个指针本身是可寻址的,更重要的是,它指向的 vertex 结构体是可寻址的。因此,你可以通过 b[0].x = 3 的方式,通过指针解引用来修改指针所指向的原始结构体的成员。
以下扩展代码进一步演示了这一点:
package main import "fmt" type vertex struct { x, y int } func main() { a := make(map[int]vertex) b := make(map[int]*vertex) v := &vertex{0, 0} a[0] = *v b[0] = v v.x, v.y = 4, 4 fmt.Println("Initial state (v modified):", a[0].x, a[0].y, b[0].x, b[0].y) // Output: Initial state (v modified): 0 0 4 4 // a[0].x = 3 // 编译错误:cannot assign to (a[0]).x // a[0].y = 3 // 编译错误:cannot assign to (a[0]).y // 对于 map[int]vertex,若要修改,必须取出副本,修改后重新存入 tempVertex := a[0] // 取出副本 tempVertex.x = 3 tempVertex.y = 3 a[0] = tempVertex // 存回 Map b[0].x = 3 // 通过指针直接修改 b[0].y = 3 // 通过指针直接修改 fmt.Println("After direct/re-assign modification:", a[0].x, a[0].y, b[0].x, b[0].y) // Output: After direct/re-assign modification: 3 3 3 3 // 再次验证通过局部变量修改的影响 u1 := a[0] // u1 是 a[0] 的一个副本 u1.x = 2 u1.y = 2 // a[0] 不受 u1 修改的影响,因为 u1 只是一个副本 fmt.Println("After u1 modification:", a[0].x, a[0].y) // Output: After u1 modification: 3 3 u2 := b[0] // u2 是 b[0] 指针的一个副本,它指向的仍然是原来的结构体 u2.x = 2 u2.y = 2 // b[0] 受 u2 修改的影响,因为它们指向同一个结构体 fmt.Println("After u2 modification:", b[0].x, b[0].y) // Output: After u2 modification: 2 2 fmt.Println("Final state:", a[0].x, a[0].y, b[0].x, b[0].y) // Output: Final state: 3 3 2 2 }
上述代码的输出与问题中提供的输出一致,清晰地展示了值类型与指针类型在Map中操作时的行为差异。
选择建议与注意事项
在决定使用 map[int]vertex 还是 map[int]*vertex 时,应考虑以下因素:
可变性需求:
- 如果你需要在Map中直接修改结构体的成员,那么必须使用 map[int]*vertex。
- 如果你只需要读取结构体的值,或者每次修改都意味着创建一个新的结构体实例并替换旧的,那么 map[int]vertex 是可行的。但请记住,修改 map[int]vertex 中的元素需要取出副本、修改、再重新赋值回Map。
内存与性能:
- 对于小型且不经常修改的结构体,map[int]vertex 可能更简单直观,因为它避免了指针的额外开销和垃圾回收器的复杂性。每次访问都会得到一个副本,这可能在某些场景下增加内存复制的开销。
- 对于大型或频繁修改的结构体,map[int]*vertex 通常是更优的选择。它避免了在每次赋值或传递时进行大结构体的内存拷贝,从而节省内存和提高性能。但这也意味着需要管理指针的生命周期,并可能引入 nil 指针的风险。
并发安全:
- 无论选择哪种,如果Map在多个goroutine之间共享,都必须考虑并发安全问题。Map本身不是并发安全的,需要使用 sync.RWMutex 或 sync.Map 来保护并发访问。
- 对于 map[int]*vertex,即使Map本身是并发安全的,指针指向的结构体也可能被多个goroutine同时修改,这需要额外的同步机制(如 sync.Mutex 内嵌在结构体中,或使用原子操作)来保护结构体内部成员。
值拷贝与引用传递的语义:
- map[int]vertex 强调值拷贝语义,每个Map条目都是独立的。
- map[int]*vertex 强调引用传递语义,多个Map条目可以指向同一个底层结构体。这使得共享数据和实现更复杂的数据结构成为可能,但也增加了数据依赖性和潜在的副作用。
总结
总而言之,map[int]vertex 和 map[int]*vertex 之间的选择取决于你的具体需求。如果你需要直接修改Map中存储的结构体实例,或者结构体较大且需要避免频繁拷贝,那么 map[int]*vertex 是首选。如果你处理的是小型、相对静态的结构体,并且可以接受取出、修改、再存入的模式,或者希望每个Map条目都是一个完全独立的副本,那么 map[int]vertex 可能更合适。理解Go语言中Map元素的地址性限制是做出正确选择的关键。
今天带大家了解了的相关知识,希望对你有所帮助;关于Golang的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~

- 上一篇
- PHP异常处理:try-catch捕获错误全解析

- 下一篇
- PyCharm激活码获取方式汇总
-
- Golang · Go教程 | 38秒前 |
- Golang反射配置解析器教程详解
- 373浏览 收藏
-
- Golang · Go教程 | 4分钟前 |
- Golang不可寻址类型有哪些?详解不可寻址值
- 253浏览 收藏
-
- Golang · Go教程 | 5分钟前 |
- Golang文件IO错误处理详解
- 201浏览 收藏
-
- Golang · Go教程 | 6分钟前 |
- Golang锁优化与并发性能提升技巧
- 277浏览 收藏
-
- Golang · Go教程 | 7分钟前 |
- Golang搭建以太坊浏览器教程详解
- 127浏览 收藏
-
- Golang · Go教程 | 13分钟前 |
- Golang并发优化与GMP调度详解
- 500浏览 收藏
-
- Golang · Go教程 | 16分钟前 |
- Golang依赖注入与wire使用教程
- 394浏览 收藏
-
- Golang · Go教程 | 19分钟前 |
- Golang单例模式:sync.Once与init对比解析
- 243浏览 收藏
-
- Golang · Go教程 | 19分钟前 |
- GolangTCP粘包处理与自定义协议详解
- 245浏览 收藏
-
- Golang · Go教程 | 22分钟前 | golang 云原生 插件开发 批处理框架 ArgoWorkflows
- 用Golang开发ArgoWorkflows插件指南
- 422浏览 收藏
-
- Golang · Go教程 | 24分钟前 |
- Golangpanicrecover使用技巧与恢复方法
- 185浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- TextIn智能文字识别平台
- TextIn智能文字识别平台,提供OCR、文档解析及NLP技术,实现文档采集、分类、信息抽取及智能审核全流程自动化。降低90%人工审核成本,提升企业效率。
- 5次使用
-
- 简篇AI排版
- SEO 简篇 AI 排版,一款强大的 AI 图文排版工具,3 秒生成专业文章。智能排版、AI 对话优化,支持工作汇报、家校通知等数百场景。会员畅享海量素材、专属客服,多格式导出,一键分享。
- 5次使用
-
- 小墨鹰AI快排
- SEO 小墨鹰 AI 快排,新媒体运营必备!30 秒自动完成公众号图文排版,更有 AI 写作助手、图片去水印等功能。海量素材模板,一键秒刷,提升运营效率!
- 5次使用
-
- Aifooler
- AI Fooler是一款免费在线AI音频处理工具,无需注册安装,即可快速实现人声分离、伴奏提取。适用于音乐编辑、视频制作、练唱素材等场景,提升音频创作效率。
- 5次使用
-
- 易我人声分离
- 告别传统音频处理的繁琐!易我人声分离,基于深度学习的AI工具,轻松分离人声和背景音乐,支持在线使用,无需安装,简单三步,高效便捷。
- 7次使用
-
- Golangmap实践及实现原理解析
- 2022-12-28 505浏览
-
- 试了下Golang实现try catch的方法
- 2022-12-27 502浏览
-
- Go语言中Slice常见陷阱与避免方法详解
- 2023-02-25 501浏览
-
- Golang中for循环遍历避坑指南
- 2023-05-12 501浏览
-
- Go语言中的RPC框架原理与应用
- 2023-06-01 501浏览