
go 语言的设计哲学之一是其简洁性,在赋值和函数参数传递方面,它严格遵循“值拷贝”原则。这意味着当你将一个变量赋值给另一个变量,或者将一个变量作为参数传递给函数时,实际上传递的是该变量值的一个副本。
然而,对于不同的数据类型,“值”的含义有所不同:
基本类型(如 int, float, bool, string 等):这些类型的值就是它们本身的数据。拷贝时会创建一个完全独立的新副本。
var a int = 10 var b int = a // b 是 a 的独立副本 b = 20 // 改变 b 不会影响 a fmt.Println(a, b) // 输出: 10 20
*指针类型(如 `T`)**:指针变量的值是它所指向内存地址。拷贝一个指针变量时,是拷贝了这个内存地址。这意味着新旧指针变量都指向同一块底层数据。
var x int = 10 var p1 *int = &x var p2 *int = p1 // p2 拷贝了 p1 的地址,两者都指向 x *p2 = 20 // 通过 p2 修改会影响 x fmt.Println(x) // 输出: 20
复合类型(如切片 []T、映射 map[K]V、通道 chan T):这些类型在 Go 内部实现时,其变量本身存储的是一个“头部”结构。
当拷贝这些复合类型变量时,拷贝的是它们的“头部”结构,即其中包含的指针值会被复制。因此,新旧变量的头部会指向同一块底层数据。这就是为什么在外部看起来像是“引用传递”或“引用拷贝”的原因,但从 Go 的角度来看,它仍然是值拷贝——拷贝的是指针或头部结构的值。
原始代码中使用的 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 操作添加的所有元素。
要解决上述问题,实现“深拷贝”,即让 NewChildPegPuzzle 返回的 PegPuzzle 实例拥有一个独立于父级 PegPuzzle 的 movesAlreadyDone 向量,我们需要显式地复制向量的内容。
首先,需要注意的是,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 就拥有了各自独立的移动历史,互不影响。
值得强调的是,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 始终采用值拷贝,但对于这些类型,拷贝的“值”是其内部的指针或头部结构,这使得新旧变量可能共享同一块底层数据。
在设计数据结构和函数时,请始终考虑你的数据是否需要独立性。如果需要,务必执行明确的深拷贝操作,以避免因共享底层数据而导致的意外副作用。
以上就是理解 Go 语言中复合类型(如切片和向量)的赋值行为:值拷贝还是引用拷贝?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号