
在 go 语言中,所有的数据传递都是值传递。这意味着当你将一个变量赋值给另一个变量,或者将一个变量作为函数参数传递时,go 都会创建一个该变量的副本。对于基本类型(如 int, string, bool 等),这很容易理解,因为副本拥有独立的数据。
然而,对于复合类型,尤其是切片(Slice),其行为可能会让人感到困惑。一个 Go 切片([]T)并不是底层数组本身,而是一个包含三个字段的结构体:
当你在 Go 中对一个切片进行赋值操作时,例如 sliceB = sliceA,复制的实际上是这个三字段的切片头信息。这意味着 sliceA 和 sliceB 现在都包含一个指向同一个底层数组的指针。因此,如果你通过 sliceB 修改了底层数组中的某个元素,sliceA 也会看到这个改变,反之亦然。这便是所谓的“共享状态”或“浅拷贝”问题。
在提供的 PegPuzzle 示例代码中,问题出在 NewChildPegPuzzle 函数的这一行:
func NewChildPegPuzzle(parent *PegPuzzle) *PegPuzzle{
retVal := new(PegPuzzle);
retVal.movesAlreadyDone = parent.movesAlreadyDone; // 问题所在
return retVal
}这里,movesAlreadyDone 是一个 *vector.Vector 类型(即指向 vector.Vector 结构体的指针)。当执行 retVal.movesAlreadyDone = parent.movesAlreadyDone; 时,复制的是 parent.movesAlreadyDone 这个指针的值。结果是,retVal.movesAlreadyDone 和 parent.movesAlreadyDone 都指向内存中的同一个 vector.Vector 实例。
因此,当你在 cp1 上调用 doMove 时:
cp1.doMove(Move{1,1,2,3});cp1.movesAlreadyDone.Push(move) 会修改这个共享的 vector.Vector 实例。随后,当你创建 cp2 并执行其 doMove 时:
cp2 = NewChildPegPuzzle(p); // 此时 cp2.movesAlreadyDone 依然指向 p.movesAlreadyDone 所指向的同一个 vector 实例
cp2.doMove(Move{3,2,5,1});cp2.movesAlreadyDone.Push(move) 同样修改的是那个共享的 vector.Vector 实例。这就是为什么 cp2 在打印时会同时显示 cp1 和 cp2 所添加的移动记录。
此外,原始代码中 InitPegPuzzle 的初始化方式也存在问题:vector.New(0) 在较新版本的 Go 中已被移除。正确的初始化方式是使用 new(vector.Vector)。
为了避免这种共享状态,我们需要执行“深拷贝”,即创建一个全新的 vector.Vector 实例,并将源 vector 的所有元素复制到新的实例中。
对于 container/vector 库,可以使用 InsertVector 方法来实现深拷贝。这个方法可以将另一个 vector 的内容插入到当前 vector 的指定位置。通过将其插入到新 vector 的起始位置,可以实现完整的复制。
以下是修正后的 PegPuzzle 相关代码:
package main
import (
"fmt"
"container/vector" // 注意:此库已在Go 1.0版本后废弃,不推荐在新项目中使用
)
type Move struct { x0, y0, x1, y1 int }
type PegPuzzle struct {
movesAlreadyDone *vector.Vector // 指向 vector.Vector 实例的指针
}
// InitPegPuzzle 初始化 PegPuzzle,创建新的 vector.Vector 实例
func (p *PegPuzzle) InitPegPuzzle(){
// 修正:使用 new(vector.Vector) 来创建新的 vector 实例
p.movesAlreadyDone = new(vector.Vector)
}
// NewChildPegPuzzle 创建一个子 PegPuzzle,并深拷贝父级的 movesAlreadyDone
func NewChildPegPuzzle(parent *PegPuzzle) *PegPuzzle{
retVal := new(PegPuzzle)
retVal.InitPegPuzzle() // 初始化子 PegPuzzle 的 movesAlreadyDone 为一个独立的空 vector
// 深拷贝:将父级 vector 的所有元素插入到子级 vector 中
// InsertVector(index, otherVector) 将 otherVector 的内容插入到当前 vector 的 index 位置
retVal.movesAlreadyDone.InsertVector(0, parent.movesAlreadyDone)
return retVal
}
func (p *PegPuzzle) doMove(move Move){
p.movesAlreadyDone.Push(move)
}
func (p *PegPuzzle) printPuzzleInfo(){
fmt.Printf("-----------START----------------------\n")
fmt.Printf("moves already done: %v\n", p.movesAlreadyDone)
fmt.Printf("------------END-----------------------\n")
}
func main() {
p := new(PegPuzzle)
p.InitPegPuzzle() // 初始化主谜题的 vector
cp1 := NewChildPegPuzzle(p) // cp1 获得 p 的 movesAlreadyDone 的深拷贝
cp1.doMove(Move{1,1,2,3})
cp1.printPuzzleInfo() // 此时 cp1 应该只包含 {1,1,2,3}
cp2 := NewChildPegPuzzle(p) // cp2 再次获得 p 的 movesAlreadyDone 的深拷贝 (此时 p 的 vector 仍为空)
cp2.doMove(Move{3,2,5,1})
cp2.printPuzzleInfo() // 此时 cp2 应该只包含 {3,2,5,1}
// 验证原始 p 的 movesAlreadyDone 是否仍为空
p.printPuzzleInfo() // 此时 p 应该仍为空
}通过上述修正,NewChildPegPuzzle 函数会为 retVal.movesAlreadyDone 创建一个全新的 vector.Vector 实例,然后将 parent.movesAlreadyDone 中的所有元素复制到这个新实例中。这样,cp1 和 cp2 就拥有了独立的 movesAlreadyDone 列表,彼此的操作不会相互影响。
sourceSlice := []int{1, 2, 3}
destSlice := make([]int, len(sourceSlice)) // 创建一个新切片,大小与源切片相同
copy(destSlice, sourceSlice) // 复制元素sourceSlice := []int{1, 2, 3}
destSlice := append([]int{}, sourceSlice...) // 创建一个空切片,然后追加源切片的所有元素Go 语言的切片赋值和传递遵循值传递原则,但由于切片本身的结构包含指向底层数据的指针,直接赋值会导致多个切片共享同一底层数据。为了实现真正的独立副本,必须执行深拷贝操作,例如使用 container/vector 库的 InsertVector 方法,或者对于现代 Go 的内置切片 ([]T),使用 copy() 函数或 append 技巧。理解这一机制是编写健壮、可预测的 Go 程序的关键。
以上就是Go 语言中切片(Vector)的赋值与复制:理解值传递与共享状态的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号