0

0

Go Map中存储结构体:值类型与指针类型的选择与影响

DDD

DDD

发布时间:2025-07-16 14:04:24

|

587人浏览过

|

来源于php中文网

原创

go map中存储结构体:值类型与指针类型的选择与影响

本文深入探讨了Go语言中将结构体作为Map值类型时,使用map[int]struct(值类型)与map[int]*struct(指针类型)的主要区别。核心在于值类型存储的是结构体副本,而指针类型存储的是结构体的引用。这种差异直接影响结构体的可变性、内存管理以及在Map中对结构体成员进行操作的方式,特别是关于Map元素地址不可取的问题,以及如何根据业务需求选择合适的存储方式。

核心区别:值语义与指针语义

在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 的方式,通过指针解引用来修改指针所指向的原始结构体的成员。

以下扩展代码进一步演示了这一点:

Sora
Sora

Sora是OpenAI发布的一种文生视频AI大模型,可以根据文本指令创建现实和富有想象力的场景。

下载
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 时,应考虑以下因素:

  1. 可变性需求:

    • 如果你需要在Map中直接修改结构体的成员,那么必须使用 map[int]*vertex。
    • 如果你只需要读取结构体的值,或者每次修改都意味着创建一个新的结构体实例并替换旧的,那么 map[int]vertex 是可行的。但请记住,修改 map[int]vertex 中的元素需要取出副本、修改、再重新赋值回Map。
  2. 内存与性能:

    • 对于小型且不经常修改的结构体,map[int]vertex 可能更简单直观,因为它避免了指针的额外开销和垃圾回收器的复杂性。每次访问都会得到一个副本,这可能在某些场景下增加内存复制的开销。
    • 对于大型或频繁修改的结构体,map[int]*vertex 通常是更优的选择。它避免了在每次赋值或传递时进行大结构体的内存拷贝,从而节省内存和提高性能。但这也意味着需要管理指针的生命周期,并可能引入 nil 指针的风险。
  3. 并发安全:

    • 无论选择哪种,如果Map在多个goroutine之间共享,都必须考虑并发安全问题。Map本身不是并发安全的,需要使用 sync.RWMutex 或 sync.Map 来保护并发访问
    • 对于 map[int]*vertex,即使Map本身是并发安全的,指针指向的结构体也可能被多个goroutine同时修改,这需要额外的同步机制(如 sync.Mutex 内嵌在结构体中,或使用原子操作)来保护结构体内部成员。
  4. 值拷贝与引用传递的语义:

    • 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结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

194

2025.06.09

golang结构体方法
golang结构体方法

本专题整合了golang结构体相关内容,请阅读专题下面的文章了解更多。

186

2025.07.04

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

313

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

523

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

49

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

190

2025.08.29

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

530

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

14

2025.12.22

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

194

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 4.1万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 2.2万人学习

Go 教程
Go 教程

共32课时 | 3.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号