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

理解 Go 语言中复合类型(如切片和向量)的赋值行为:值拷贝还是引用拷贝?

胖明大大_8569

胖明大大_8569

发布时间:2025-07-10 19:32:19

|

1021人浏览过

|

来源于php中文网

原创

理解 Go 语言中复合类型(如切片和向量)的赋值行为:值拷贝还是引用拷贝?

Go 语言在赋值操作中始终遵循“值拷贝”原则,但对于切片(slice)、映射(map)和通道(channel)等复合类型,以及指针类型,所拷贝的“值”是其内部结构(如切片头或指针地址),而非其底层数据。这意味着直接赋值这些类型时,新旧变量可能指向同一块底层数据,导致修改一个变量会影响另一个。要实现底层数据的独立拷贝(深拷贝),需要显式地复制数据内容。

Go 语言的赋值机制:值拷贝的真相

go 语言的设计哲学之一是其简洁性,在赋值和函数参数传递方面,它严格遵循“值拷贝”原则。这意味着当你将一个变量赋值给另一个变量,或者将一个变量作为参数传递给函数时,实际上传递的是该变量值的一个副本。

然而,对于不同的数据类型,“值”的含义有所不同:

  1. 基本类型(如 int, float, bool, string 等):这些类型的值就是它们本身的数据。拷贝时会创建一个完全独立的新副本。

    var a int = 10
    var b int = a // b 是 a 的独立副本
    b = 20        // 改变 b 不会影响 a
    fmt.Println(a, b) // 输出: 10 20
  2. *指针类型(如 `T`)**:指针变量的值是它所指向内存地址。拷贝一个指针变量时,是拷贝了这个内存地址。这意味着新旧指针变量都指向同一块底层数据。

    var x int = 10
    var p1 *int = &x
    var p2 *int = p1 // p2 拷贝了 p1 的地址,两者都指向 x
    *p2 = 20         // 通过 p2 修改会影响 x
    fmt.Println(x)   // 输出: 20
  3. 复合类型(如切片 []T、映射 map[K]V、通道 chan T):这些类型在 Go 内部实现时,其变量本身存储的是一个“头部”结构。

    • 切片头部:包含一个指向底层数组的指针、切片的长度和容量。
    • 映射头部:包含一个指向底层哈希表结构的指针。
    • 通道头部:包含一个指向通道运行时数据结构的指针。

    当拷贝这些复合类型变量时,拷贝的是它们的“头部”结构,即其中包含的指针值会被复制。因此,新旧变量的头部会指向同一块底层数据。这就是为什么在外部看起来像是“引用传递”或“引用拷贝”的原因,但从 Go 的角度来看,它仍然是值拷贝——拷贝的是指针或头部结构的值。

案例分析:container/vector 的“引用”假象

原始代码中使用的 container/vector 类型,其行为与现代 Go 语言中的切片([]T)非常相似。PegPuzzle 结构体中的 movesAlreadyDone 字段被定义为 *vector.Vector,即一个指向 vector.Vector 对象的指针。

让我们分析一下原始代码中导致“引用”假象的关键部分:

type PegPuzzle struct {
    movesAlreadyDone * vector.Vector;
}

// ...

func NewChildPegPuzzle(parent *PegPuzzle) *PegPuzzle{
    retVal := new(PegPuzzle);
    // 问题所在:这里拷贝的是 parent.movesAlreadyDone 指针的值
    // 两个 PegPuzzle 实例的 movesAlreadyDone 字段将指向同一个 vector.Vector 对象
    retVal.movesAlreadyDone = parent.movesAlreadyDone;
    return retVal
}

func (p *PegPuzzle) doMove(move Move){
    // 对其中一个 PegPuzzle 调用 doMove,实际上是修改了共享的 vector.Vector 对象
    p.movesAlreadyDone.Push(move);
}

当 cp1 = NewChildPegPuzzle(p) 执行时,cp1.movesAlreadyDone 被赋值为 p.movesAlreadyDone 的值。由于 p.movesAlreadyDone 是一个 *vector.Vector 类型的指针,这里拷贝的是这个指针所指向的内存地址。因此,cp1 和 p(以及后续的 cp2)的 movesAlreadyDone 字段都指向了同一个 vector.Vector 实例。

当 cp1.doMove(Move{1,1,2,3}) 被调用时,它通过 cp1.movesAlreadyDone 修改了共享的 vector.Vector 对象。接着,当 cp2 = NewChildPegPuzzle(p) 再次被调用时,cp2.movesAlreadyDone 也指向了同一个 vector.Vector 对象。因此,cp2.doMove(Move{3,2,5,1}) 也会修改这个共享对象。最终,打印 cp2.movesAlreadyDone 时,会发现它包含了两次 doMove 操作添加的所有元素。

实现 container/vector 的深度拷贝

要解决上述问题,实现“深拷贝”,即让 NewChildPegPuzzle 返回的 PegPuzzle 实例拥有一个独立于父级 PegPuzzle 的 movesAlreadyDone 向量,我们需要显式地复制向量的内容。

AI舌诊面诊问诊医疗系统+诊疗商城系统源码
AI舌诊面诊问诊医疗系统+诊疗商城系统源码

AI 舌诊面诊问诊医疗系统 + 诊疗商城系统源码是一套结合 AI 辅助诊断与商品转化的健康类程序,基于浩创云、ThinkPHP(PHP)开发,支持微信服务号与 H5 端,适合做中医舌诊面诊问答、健康建议推送以及配套诊疗商品的运营。

下载

首先,需要注意的是,container/vector 包中的 vector.New(0) 构造函数在 Go 的新版本中已被移除。正确的做法是使用 new(vector.Vector) 来创建一个新的 vector.Vector 实例。

修正后的 InitPegPuzzle 方法应如下:

package main

import (
    "fmt"
    "container/vector" // 注意:此包在现代Go中已不推荐使用
)

type Move struct { x0, y0, x1, y1 int }

type PegPuzzle struct {
    movesAlreadyDone * vector.Vector;
}

func (p *PegPuzzle) InitPegPuzzle(){
    // 正确的初始化方式:创建新的 vector.Vector 实例
    p.movesAlreadyDone = new (vector.Vector);
}

接下来,为了实现 NewChildPegPuzzle 的深度拷贝,我们需要为新的 PegPuzzle 实例创建一个全新的 vector.Vector,然后将父级向量中的所有元素复制到这个新向量中。container/vector 提供了 InsertVector 方法,可以用来将一个向量的内容插入到另一个向量中。

func NewChildPegPuzzle(parent *PegPuzzle) *PegPuzzle{
    retVal := new (PegPuzzle);
    // 1. 为新的 PegPuzzle 实例初始化一个独立的 vector.Vector
    retVal.InitPegPuzzle ();
    // 2. 将父级向量的内容深度拷贝到新的向量中
    // InsertVector(index, otherVector) 会将 otherVector 的所有元素插入到当前向量的指定索引处
    retVal.movesAlreadyDone.InsertVector (0, parent.movesAlreadyDone);
    return retVal
}

// 主函数保持不变,但现在 cp1 和 cp2 将拥有独立的 movesAlreadyDone 向量
func main() {
    p := new(PegPuzzle);
    p.InitPegPuzzle(); // 初始化父级谜题的向量

    cp1 := NewChildPegPuzzle(p);
    cp1.doMove(Move{1,1,2,3});
    cp1.printPuzzleInfo(); // cp1 包含自己的移动

    cp2 := NewChildPegPuzzle(p);
    cp2.doMove(Move{3,2,5,1});
    cp2.printPuzzleInfo(); // cp2 包含自己的移动,不含 cp1 的移动
}

通过上述修改,NewChildPegPuzzle 函数现在会创建一个全新的 PegPuzzle 实例,并为其 movesAlreadyDone 字段分配一个独立的 vector.Vector 对象。然后,它会将父级 PegPuzzle 的 movesAlreadyDone 向量中的所有元素复制到这个新的向量中。这样,cp1 和 cp2 就拥有了各自独立的移动历史,互不影响。

现代 Go 语言中的切片 (Slice) 拷贝

值得强调的是,container/vector 包在现代 Go 语言中已经不推荐使用,其功能已被内置的切片([]T)类型所取代。切片提供了更强大、更灵活且更高效的数据结构。

切片与 container/vector 类似,其赋值行为也是拷贝切片头。要对切片进行深度拷贝,最常用的方法是使用 Go 内置的 copy() 函数或手动遍历元素。

切片赋值行为示例:

package main

import "fmt"

func main() {
    originalSlice := []int{1, 2, 3}
    assignedSlice := originalSlice // 拷贝切片头,两者指向同一底层数组

    assignedSlice[0] = 99 // 修改 assignedSlice 会影响 originalSlice

    fmt.Println("Original Slice:", originalSlice) // 输出: Original Slice: [99 2 3]
    fmt.Println("Assigned Slice:", assignedSlice) // 输出: Assigned Slice: [99 2 3]
}

切片深度拷贝示例:

要实现切片的深度拷贝,我们需要创建一个新的切片,并使用 copy() 函数将原切片的内容复制到新切片中。copy(dst, src) 函数会从 src 切片复制元素到 dst 切片,复制的元素数量是 len(dst) 和 len(src) 中的较小值。

package main

import "fmt"

func main() {
    originalSlice := []int{1, 2, 3}

    // 方法一:使用 make 创建一个与原切片长度相同的新切片,然后拷贝
    copiedSlice1 := make([]int, len(originalSlice))
    copy(copiedSlice1, originalSlice)

    // 方法二:使用 append 技巧(对于简单类型通常有效)
    // copiedSlice2 := append([]int(nil), originalSlice...) // 更简洁,但可能创建不必要的容量
    // 或者直接初始化一个空切片然后 append
    copiedSlice2 := []int{}
    copiedSlice2 = append(copiedSlice2, originalSlice...)

    copiedSlice1[0] = 99
    copiedSlice2[1] = 88

    fmt.Println("Original Slice:", originalSlice) // 输出: Original Slice: [1 2 3]
    fmt.Println("Copied Slice 1:", copiedSlice1) // 输出: Copied Slice 1: [99 2 3]
    fmt.Println("Copied Slice 2:", copiedSlice2) // 输出: Copied Slice 2: [1 88 3]
}

对于包含复杂类型(如结构体指针)的切片,copy() 函数执行的是浅拷贝,即它只拷贝了指针本身,而不是指针指向的数据。在这种情况下,如果需要深度拷贝,你可能需要手动遍历切片,并对每个元素进行递归的深度拷贝。

总结与最佳实践

理解 Go 语言中复合类型(尤其是切片和指针)的赋值行为至关重要。虽然 Go 始终采用值拷贝,但对于这些类型,拷贝的“值”是其内部的指针或头部结构,这使得新旧变量可能共享同一块底层数据。

  • 浅拷贝(默认行为):直接赋值切片、映射或指针,会复制其头部或地址,导致多个变量引用同一份底层数据。修改其中一个变量可能影响所有引用该数据的变量。
  • 深拷贝(手动实现):当需要确保数据独立性时,必须显式地创建新的底层数据结构,并将原有数据内容复制过去。对于切片,推荐使用 make 配合 copy() 函数。对于 container/vector,可以使用 InsertVector。
  • 弃用 container/vector:在现代 Go 开发中,应优先使用内置的切片 ([]T) 代替 container/vector,因为它更高效、更符合 Go 的设计理念。

在设计数据结构和函数时,请始终考虑你的数据是否需要独立性。如果需要,务必执行明确的深拷贝操作,以避免因共享底层数据而导致的意外副作用。

热门AI工具

更多
咔片AIPPT

一款在线AI演示文稿制作工具,可根据主题和内容需求辅助生成PPT结构与页面,提高演示材料制作效率。

WorkBuddy

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

SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

DeepSeek

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

UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

豆包大模型

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

火山引擎

火山引擎是一款面向企业的云计算与AI服务平台。

蛙蛙写作

一款AI论文写作工具,主要用于超级AI智能写作助手,适合需要提升相关任务效率的用户。

音述AI
音述AI Hot

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

相关专题

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

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

3071

2023.10.31

php数据类型
php数据类型

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

554

2025.10.31

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

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

522

2026.02.12

string转int
string转int

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

6579

2023.08.02

css中float用法
css中float用法

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

6767

2024.04.28

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

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

784

2025.10.23

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

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

4854

2025.06.09

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

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

5011

2025.07.04

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

20

2026.10.22

热门下载

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

精品课程

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

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