讲师中心 微信公众号
AI工具推荐 视频效率加速

深入理解Go语言Map中结构体的存储:值类型与指针类型的选择与影响

雨静同学_2760

雨静同学_2760

发布时间:2025-07-16 14:02:02

|

1033人浏览过

|

来源于php中文网

原创

深入理解Go语言Map中结构体的存储:值类型与指针类型的选择与影响

本文详细解析Go语言中Map存储结构体时,map[int]struct(值类型)与map[int]*struct(指针类型)的差异。map[int]struct存储的是结构体副本,其成员不可直接修改;而map[int]*struct存储的是结构体引用,允许通过指针直接修改原结构体。文章将通过代码示例阐明这两种方式在数据修改行为上的根本区别及适用场景,以帮助开发者做出明智选择。

Go语言中值类型与指针类型的基础

在go语言中,数据类型可以分为值类型和指针类型。理解它们在map中的行为差异是至关重要的。

  • 值类型 (Value Type):当一个值类型的数据被赋值给另一个变量时,会创建一个该数据的新副本。对新副本的修改不会影响原始数据。常见的Go值类型包括基本数据类型(如int, float, bool, string)、数组和结构体(当它们不包含指针字段时,或者即使包含,也是复制指针本身而非其指向的数据)。
  • 指针类型 (Pointer Type):指针存储的是另一个变量的内存地址。当一个指针被赋值给另一个变量时,它们都指向内存中的同一个底层数据。通过任一指针修改底层数据,会影响所有指向该数据的指针。

Map中存储结构体的行为差异

当我们将结构体作为Map的值类型时,选择map[int]vertex或map[int]*vertex会带来截然不同的行为。

1. map[int]vertex:存储结构体副本

当Map的值类型是结构体本身时(例如map[int]vertex),Map中存储的是结构体的副本。这意味着:

  • 赋值行为:当您将一个结构体赋值给Map中的某个键时,Map会复制该结构体的所有字段,并将这个副本存储起来。原始结构体与Map中存储的副本是完全独立的。
  • 修改行为:您不能直接修改Map中已存储的结构体成员。例如,a[0].x = 3这样的操作会导致编译错误。这是因为Go语言的Map元素是不可寻址的(unaddressable)。Map的底层实现可能会在内存中移动元素,如果允许直接修改,可能会导致悬空指针或数据不一致。 如果需要修改map[int]vertex中的结构体,您必须先取出该结构体的副本,修改副本,然后再将修改后的副本重新赋值回Map。

2. map[int]*vertex:存储结构体指针

当Map的值类型是结构体指针时(例如map[int]*vertex),Map中存储的是结构体的内存地址。这意味着:

  • 赋值行为:当您将一个结构体指针赋值给Map中的某个键时,Map会存储这个指针本身。Map中的元素和原始结构体变量都指向内存中的同一个底层结构体。
  • 修改行为:您可以直接通过Map中存储的指针来修改底层结构体的成员。例如,b[0].x = 3是完全合法的,它会修改b[0]所指向的vertex结构体的x字段。

代码示例与解析

为了更清晰地展示这两种方式的区别,我们来看一个具体的Go语言程序:

立即学习“go语言免费学习笔记(深入)”;

package main

import "fmt"

type vertex struct {
    x, y int
}

func main() {
    // 声明两个Map:a 存储 vertex 结构体值,b 存储 *vertex 结构体指针
    a := make(map[int]vertex)
    b := make(map[int]*vertex)

    // 创建一个 vertex 结构体实例,并获取其指针
    v := &vertex{0, 0} 

    // 将 v 指向的结构体的值(副本)存入 a[0]
    a[0] = *v 
    // 将 v 这个指针本身存入 b[0]
    b[0] = v 

    // 第一次修改:通过原始指针 v 修改其指向的结构体
    // 此时,v 和 b[0] 都指向同一个内存地址,a[0] 是 v 的一个独立副本
    v.x, v.y = 4, 4
    fmt.Printf("修改原始v后: a[0]: {%d, %d}, b[0]: {%d, %d}\n", a[0].x, a[0].y, b[0].x, b[0].y)

    // 尝试直接修改 map[int]vertex 中的元素 - 编译错误
    // a[0].x = 3 // 编译错误: cannot assign to (a[0]).x
    // a[0].y = 3 // 编译错误: cannot assign to (a[0]).y

    // 正确修改 map[int]vertex 中元素的方式:取出、修改、重新赋值
    tempA := a[0] // 取出 a[0] 的副本
    tempA.x = 3
    tempA.y = 3
    a[0] = tempA  // 将修改后的副本重新赋值回 a[0]
    fmt.Printf("通过取出-修改-赋值修改a[0]后: a[0]: {%d, %d}, b[0]: {%d, %d}\n", a[0].x, a[0].y, b[0].x, b[0].y)

    // 修改 map[int]*vertex 中的元素 - 可以直接修改
    // b[0] 是一个指针,直接修改其指向的结构体成员
    b[0].x = 3 
    b[0].y = 3 
    fmt.Printf("直接修改b[0]指向的结构体后: a[0]: {%d, %d}, b[0]: {%d, %d}\n", a[0].x, a[0].y, b[0].x, b[0].y)

    // 观察从Map中取出元素后的修改行为
    u1 := a[0] // u1 是 a[0] 的一个值副本
    u1.x = 2 
    u1.y = 2 
    // u1 的修改不影响 a[0],因为 u1 是 a[0] 的一个独立副本
    fmt.Printf("u1 (a[0]的副本) 修改后: a[0]: {%d, %d}, b[0]: {%d, %d}\n", a[0].x, a[0].y, b[0].x, b[0].y)

    u2 := b[0] // u2 是 b[0] 的一个指针副本,它们都指向同一个底层结构体
    u2.x = 2 
    u2.y = 2 
    // u2 的修改通过指针影响了 b[0] 指向的底层结构体
    fmt.Printf("u2 (b[0]的指针副本) 修改后: a[0]: {%d, %d}, b[0]: {%d, %d}\n", a[0].x, a[0].y, b[0].x, b[0].y)
}

输出结果:

修改原始v后: a[0]: {0, 0}, b[0]: {4, 4}
通过取出-修改-赋值修改a[0]后: a[0]: {3, 3}, b[0]: {4, 4}
直接修改b[0]指向的结构体后: a[0]: {3, 3}, b[0]: {3, 3}
u1 (a[0]的副本) 修改后: a[0]: {3, 3}, b[0]: {3, 3}
u2 (b[0]的指针副本) 修改后: a[0]: {3, 3}, b[0]: {2, 2}

解析:

Go语言(Golang)1.26.0
Go语言(Golang)1.26.0

Go语言(Golang)1.26.0版本官方下载,版本号 1.26.0,适合旧项目维护、兼容性测试和指定版本开发环境搭建。

下载
  1. v.x, v.y = 4, 4 后:

    • a[0] 仍然是 {0, 0},因为 a[0] = *v 存储的是 v 在 {0, 0} 时的一个副本,与 v 后来的变化无关。
    • b[0] 变为 {4, 4},因为 b[0] = v 存储的是 v 的指针,v 所指向的内存地址中的值被修改为 {4, 4},所以 b[0] 也反映了这一变化。
  2. a[0].x = 3 编译错误的原因: 如前所述,Go语言的Map元素是不可寻址的。这意味着您不能获取Map中元素的内存地址,因此也无法直接对其进行成员修改。如果您尝试这样做,编译器会报错。正确的做法是先将元素取出(这会得到一个副本),修改副本,然后将修改后的副本重新赋值回Map。

  3. b[0].x = 3 可以的原因:b[0] 存储的是一个指针。通过指针访问结构体成员是合法的,因为您实际上是在操作指针所指向的内存区域,而不是Map本身存储的那个指针值。

  4. u1 := a[0] 与 u2 := b[0] 的区别:

    • u1 := a[0] 得到了 a[0] 存储的 vertex 结构体的一个完整副本。对 u1 的修改只影响 u1 这个副本,不影响 a[0] 中存储的原始副本。
    • u2 := b[0] 得到了 b[0] 存储的 *vertex 指针的一个副本。这意味着 u2 和 b[0] 现在都指向内存中的同一个 vertex 结构体。因此,通过 u2 修改结构体成员,也会影响到 b[0] 所引用的结构体。

选择建议与注意事项

理解了值类型和指针类型在Map中的行为后,我们可以根据实际需求做出选择:

何时使用 map[int]struct (值类型)

  • 结构体较小:如果结构体包含的字段不多,复制开销很小,那么使用值类型可以简化内存管理。
  • 数据隔离:当您希望Map中的每个元素都是独立的副本,对它们的修改不会影响到Map外部的原始结构体,或者不希望通过Map元素来修改共享数据时,值类型是更好的选择。
  • 无需频繁原地修改:如果Map中的结构体一旦存入就不再需要修改其内部成员,或者修改频率很低,且可以接受“取出-修改-重新赋值”的模式,那么值类型是合适的。

何时使用 map[int]*struct (指针类型)

  • 结构体较大:如果结构体包含大量字段或占用内存较大,使用指针可以避免不必要的深拷贝,从而提高性能和减少内存消耗。
  • 需要频繁原地修改:当您需要频繁地修改Map中已存在的结构体实例的成员时,使用指针类型可以允许直接修改,而无需取出和重新赋值的步骤,代码更简洁高效。
  • 共享数据:当多个部分需要引用和操作同一个结构体实例时,使用指针可以实现数据共享,保持数据的一致性。
  • 实现接口:某些情况下,如果结构体需要实现某个接口,并且接口方法需要修改结构体自身,那么Map中存储指针会更方便。

注意事项

  • *空指针问题 (`map[int]struct)**:当使用指针类型时,需要注意空指针(nil)的可能性。如果您从Map中取出一个指针,但该键不存在,或者存储的值是nil`,尝试访问其成员会导致运行时错误(panic)。
    var p *vertex
    p = b[100] // 如果 b[100] 不存在,p 将是 nil
    if p != nil {
        fmt.Println(p.x)
    } else {
        fmt.Println("键不存在或值为nil")
    }
  • 并发安全:Go语言的Map本身不是并发安全的。无论您存储的是值类型还是指针类型,在多个goroutine同时读写Map时,都需要通过互斥锁(sync.Mutex)或其他并发原语来保护Map的访问,以避免数据竞争。即使您存储的是指针,对指针指向的数据的修改也可能需要同步。

总结

选择在Go语言Map中存储结构体的值类型还是指针类型,取决于您的具体应用场景、性能需求以及对数据修改行为的期望。

  • map[int]struct 提供了数据隔离,每次操作都是对副本进行,但不能直接修改Map中的元素。适用于结构体较小、数据不需共享或不频繁修改的场景。
  • *`map[int]struct`** 允许直接修改Map中元素所指向的底层结构体,避免了大量数据拷贝,实现了数据共享。适用于结构体较大、需要频繁原地修改或多处共享同一实例的场景。

理解这两种方式的底层机制和行为差异,是编写高效、健壮Go程序的关键。

热门AI工具

更多
DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

立刻MV
立刻MV Hot

立刻MV是一款AI文本写作工具,AI 音乐视频(MV)创作工具。

创客贴AI

创客贴AI是一款AI图像与设计工具,AI辅助的智能在线设计工具。

Flowith
Flowith Hot

Flowith是一款基于画布和智能体工作流的 AI 协作工作空间。

小橡皮AI

一款AI工具,主要用于小橡皮一键去AI味,将机器味重的内容改得更有活人感,防止因为 AI 内容被平台打上 AI 标签,以及限流,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

Atoms
Atoms Hot

Atoms是一款AI智能体工具,第一支自动构建真实业务的 AI 团队。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

相关专题

更多
数据类型有哪几种
数据类型有哪几种

数据类型有整型、浮点型、字符型、字符串型、布尔型、数组、结构体和枚举等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

3351

2023.10.31

php数据类型
php数据类型

本专题整合了php数据类型相关内容,阅读专题下面的文章了解更多详细内容。

554

2025.10.31

c语言 数据类型
c语言 数据类型

本专题整合了c语言数据类型相关内容,阅读专题下面的文章了解更多详细内容。

562

2026.02.12

string转int
string转int

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

7139

2023.08.02

css中float用法
css中float用法

css中float属性允许元素脱离文档流并沿其父元素边缘排列,用于创建并排列、对齐文本图像、浮动菜单边栏和重叠元素。想了解更多float的相关内容,可以阅读本专题下面的文章。

7327

2024.04.28

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

864

2025.10.23

golang结构体相关大全
golang结构体相关大全

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

5214

2025.06.09

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

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

5391

2025.07.04

DBeaver怎么导入数据
DBeaver怎么导入数据

DBeaver数据导入专题,整理Excel、CSV、SQL、TXT等常见文件的数据导入方法,涵盖目标数据库选择、数据表创建、字段映射、数据类型设置及导入参数配置等操作,帮助用户将本地文件中的数据写入数据库,并了解批量导入、重复数据处理和导入失败等常见问题的解决方法。

20

2026.11.05

热门下载

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

精品课程

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn