本文主要介绍Go实现浅拷贝和深拷贝,以及工具库mohae/deepcopy。

定义

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。比较典型的就是Value(值)对象,如预定义类型Int32,Double,以及结构(struct),枚举(Enum)等

浅拷贝

go使用copy内置函数将源片中的元素复制到目标切片,如果拷贝的 结构中不包含指针是没问题,否则则数据源和拷贝之间对应指针会共同指向同一块内存。

func shallowCopyReference() {
	brazil := []*Player{{"Neymar"}, {"Messi"}}
	argentina := make([]*Player, len(brazil))
	copy(argentina, brazil)
	fmt.Printf("%p, %p\n", brazil, brazil[0])
	fmt.Printf("%p, %p\n", argentina, argentina[0])
	brazil[0].Name = "Anthony"
	fmt.Printf("%p, %v, %p\n", brazil, brazil[0], brazil[0])
	fmt.Printf("%p, %v, %p\n", argentina, argentina[0], brazil[0])
}
//output
0xc0000412e0, 0xc0000412f0
0xc000041310, 0xc0000412f0
0xc0000412e0, &{Anthony}, 0xc0000412f0
0xc000041310, &{Anthony}, 0xc0000412f0 

如上,修改了src(拷贝原始结构)的指针对象,dst(拷贝目标结构)的值也被改动了,显然是不行的。

深拷贝

方法一:gob、json 序列化成字节序列再反序列化生成克隆对象

  • gob序列化
func deepCopyByGobExample() {
	players := append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})
	brazil := &Team{Players: players}
	argentina := &Team{}
	err := deepCopyByGob(brazil, argentina)
	if err != nil {
		fmt.Errorf("deepcopy error:", err)
	}
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}

func deepCopyByGob(dst, src *Team) error {
	var buf bytes.Buffer
	if err := gob.NewEncoder(&buf).Encode(src); err != nil {
		return err
	}
	return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}
//output
brazil,&{Neymar}, 0xc000184ff0
argentina,&{Neymar}, 0xc000185330
brazil, &{Anthony}, 0xc000184ff0
argentina, &{Neymar}, 0xc000184ff0 
  • json序列化
func deepcopyByJsonExample() {
	players := append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})
	brazil := &Team{Players: players}
	argentina := &Team{}
	err := deepCopyByJson(brazil, argentina)
	if err != nil {
		fmt.Errorf("deepcopy error:", err)
	}
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}

func deepCopyByJson(src, dst *Team) error {
	if tmp, err := json.Marshal(src); err != nil {
		return err
	} else {
		err = json.Unmarshal(tmp, dst)
		return err
	}
}
//output
brazil,&{Neymar}, 0xc0000412e0
argentina,&{Neymar}, 0xc000041490
brazil, &{Anthony}, 0xc0000412e0
argentina, &{Neymar}, 0xc0000412e0

可以看到deepcopy出来的指针地址不一样,那么都dst的修改就不会影响到src了。

方法二:使用第三方工具库

DeepCopy对事物进行深度复制:未导出的字段值不会被复制。 feature:

  • copy slice
  • copy map
func deepCopyExample() {
	brazil := &Team{Players: append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})}
	argentinaIfc := deepcopy.Copy(brazil)
	argentina := argentinaIfc.(*Team)
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}
//output
brazil,&{Neymar}, 0xc0000412e0
argentina,&{Neymar}, 0xc000041330
brazil, &{Anthony}, 0xc0000412e0
argentina, &{Neymar}, 0xc0000412e0

小结

浅拷贝是值的拷贝,如果属性是引用,拷贝的就是内存地址,需要使用深拷贝。深拷贝的实现原理本质上是通过反射实现。通过将源对象转换成接口,再对接口通过反射判断其类型。所以可以通过序列化成gob、json等来实现。工具库mohae/deepcopy可以对切片、map、结构体、接口进行深拷贝,是个不错的选择。

参考

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy