Go中的深拷贝和浅拷贝

Go中的数据可以分为值类型和引用类型,值类型存储的是具体的值,引用类型存储的是指向某个地址的指针。

如下图所示,变量a代表的地址A中存储的就是值111,存储的是值本身,所以通过变量能直接拿到值。变量b代表的地址B中不是直接存储的值,而是存储的一个指针(指向地址=A),通过地址B能间接拿到地址A中存储的值。

Go中的值类型和引用类型有:

  • 值类型:数组、结构体、布尔值、数字、字符串
  • 引用类型:切片、映射、通道、指针、函数、接口

浅拷贝与深拷贝的不同在于对引用类型的处理,浅拷贝遇到引用类型时,会复制指针,而深拷贝遇到引用类型时,会复制引用类型指向的具体的值。

浅拷贝

赋值语句

数组是值类型,切片是引用类型,对数组赋值是复制了一份值到新的变量中,对切片赋值时是复制的指针,所以对其中一个变量的修改会影响到另一个变量指向的值。

数组,修改不互相影响:

go 复制代码
	a1 := [3]int{10, 20, 30}
	a2 := a1
	a2[0] = 100
	fmt.Println(a1) // [10 20 30]
	fmt.Println(a2) // [100 20 30]

切片,修改的是 指针指向的同一个值 所以会相互影响:

go 复制代码
	s1 := []int{1, 2, 3, 4, 5}
	s2 := s1
	s2[0] = 100
	fmt.Println(s1) // [100 2 3 4 5]
	fmt.Println(s2) // [100 2 3 4 5]

copy

copy是用于复制切片的函数,会将一个切片复制到另一个切片中,copy比直接赋值好一点,会复制切片中的值到新的切片中:

go 复制代码
	s3 := make([]int, len(s1))
	copy(s3, s1) // 将s1的值复制到s3
	s3[0] = 500
	fmt.Println(s1) // [100 2 3 4 5]
	fmt.Println(s3) // [500 2 3 4 5]

所以s3和s1也是不互相影响的。

但是对于切片中包含引用类型的情况,copy复制的是引用类型的地址,所以还是会有影响:

go 复制代码
	s4 := [][]int{
		{1, 2, 3},
		{4, 5},
	}
	s5 := make([][]int, len(s4))
	copy(s5, s4)

	s5[0][0] = 100
	fmt.Println(s4) // [[100 2 3] [4 5]]
	fmt.Println(s5) // [[100 2 3] [4 5]]

这时候需要进行再一层的复制才行:

go 复制代码
	s6 := make([][]int, len(s4))
	for i, val := range s4 {
		item := make([]int, len(val))
		copy(item, val)
		s6[i] = item
	}
	s6[0][0] = 500
	fmt.Println(s4) // [[100 2 3] [4 5]]
	fmt.Println(s6) // [[500 2 3] [4 5]]

这里用到了make,顺带一提makenew的区别,看它们的注释就能知道了:

go 复制代码
  // The make built-in function allocates and initializes an object of type slice, map, or chan (only).
  // Like new, the first argument is a type, not a value.
  // Unlike new, make's return type is the same as the type of its argument, not a pointer to it.
  x1 := make([]int, 10)
  
  // The new built-in function allocates memory.
  // The first argument is a type, not a value, and the value returned is a pointer to a newly allocated zero value of that type.
  x2 := new([]int)

makenew都是用于分配内存,第一个参数都是数据类型,不同点在于:

  • make只用于类型slicemapchan,而new用于更多类型。
  • make传入的是什么类型,返回的就是什么类型的值,而new返回的是指向传入的类型的指针类型。

深拷贝

使用json序列化

使用json.Marshal(序列化)json.UnMarshal(反序列化),先用json.Marshal将数据编码为json字符串,然后用json.UnMarshal将数据反编码为对应的值。

go 复制代码
	s7 := [][][]int{
		{{1, 2, 3}, {4, 5}},
		{{6}},
	}
	var s8 [][][]int
	b, _ := json.Marshal(s7)
	json.Unmarshal(b, &s8)

	s8[0][0][0] = 100

	fmt.Println(s7) // [[[1 2 3] [4 5]] [[6]]]
	fmt.Println(s8) // [[[100 2 3] [4 5]] [[6]]]

这种方法只对可导出的字段生效,对于不可导出的字段不能进行json序列化和反序列化。也不能对函数类型进行序列化和反序列化(当然一般不会把函数类型作为数据来使用)。

go 复制代码
	d1 := Data{
		SliceData: []int{1, 2, 3},
		mapData:   map[string]int{"A": 1},
	}
	var d2 Data
	b1, _ := json.Marshal(d1)
	json.Unmarshal(b1, &d2)
	d2.SliceData[0] = 100
	fmt.Println(d1) // {[1 2 3] map[A:1]}
	fmt.Println(d2) // {[100 2 3] map[]}

手动复制

创建一个新的对象,手动将值复制到新对象中。手动复制不论字段是导出字段还是不可导出字段都可以复制。

go 复制代码
	d1 := Data{
		SliceData: []int{1, 2, 3},
		mapData:   map[string]int{"A": 1},
	}

	var d3 Data
	sliceData := make([]int, len(d1.SliceData))
	// 这个切片的复制可以直接用copy(sliceData, d1.SliceData) 替换
	for i, v := range d1.SliceData {
		sliceData[i] = v
	}
	d3.SliceData = sliceData
	d3.SliceData[0] = 500

	mapData := make(map[string]int)
	for key, val := range d1.mapData {
		mapData[key] = val
	}
	d3.mapData = mapData
	d3.mapData["B"] = 2

	fmt.Println(d1) // {[1 2 3] map[A:1]}
	fmt.Println(d3) // {[500 2 3] map[A:1 B:2]}

封装通用的方法

手动复制的过程中一些方法其实是通用的,把这些方法抽象出来作为通用的方法可以提高效率。

go 复制代码
package deepcopy

// 复制指针
// 使用 ... 是为了使函数copier作为可选参数,而不是必传的
func CopyPointer[T any](original *T, copier ...func(T) T) *T {
	if original == nil {
		return nil
	}
	var cp T
	if len(copier) > 0 {
		cp = copier[0](*original)
	} else {
		cp = *original
	}

	return &cp
}

// 复制切片
func CopySlice[T any](original []T, copier ...func(T) T) []T {
	if original == nil {
		return nil
	}

	var cs = make([]T, len(original))

	for i, val := range original {
		if len(copier) > 0 {
			cs[i] = copier[0](val)
		} else {
			cs[i] = val
		}
	}
	return cs
}

// 复制映射
func CopyMap[K comparable, V any](original map[K]V, copier ...func(V) V) map[K]V {
	if original == nil {
		return nil
	}
	cm := make(map[K]V)
	for key, val := range original {
		if len(copier) > 0 {
			cm[key] = copier[0](val)
		} else {
			cm[key] = val
		}
	}
	return cm
}

复制的时候这样使用:

go 复制代码
package deepcopy

import "fmt"

func init() {
	type Data struct {
		PointerData *int
		SliceData   []int
		mapData     map[string]int
	}
	var num = 1
	p1 := &num

	d5 := Data{
		PointerData: p1,
		SliceData:   []int{1, 2, 3},
		mapData:     map[string]int{"A": 1},
	}
	d6 := Data{
		PointerData: CopyPointer[int](d5.PointerData),
		SliceData:   CopySlice[int](d5.SliceData),
		mapData:     CopyMap[string, int](d5.mapData),
	}

	var num1 = 100
	p2 := &num1

	d6.PointerData = p2
	d6.SliceData[0] = 500
	d6.mapData["B"] = 1000

	fmt.Println(d5) // {0xc0000ac018 [1 2 3] map[A:1]}
	fmt.Println(d6) // {0xc0000ac048 [500 2 3] map[A:1 B:1000]}
}

更通用的方法(暂时无法实现)

尝试通过递归实现下面这种方法,直接将值传入就能得到一个深拷贝的值,但是无法实现。因为无法通过反射拿到具体的类型作为函数的类型参数。

go 复制代码
func DeepCopyAny[T any](val T) T

比如:

go 复制代码
// 拿不到类型中的类型,比如切片中每个元素的类型
func copier[T any](val T) T {
	var empty T
	// 通过反射拿到数据的类型,比如slice、struct、map、pointer等
	vv := reflect.ValueOf(val)
	kind := vv.Kind()

	// 根据不同的类型选择不同的复制函数
	switch {
	case kind == reflect.Slice:
		if vv.Len() == 0 {
			return empty
		}
		vvItem := vv.Index(0)
		// 拿到元素的类型
		// reflect.TypeOf(vvItem) is not a typecompilerNotAType
		// func reflect.TypeOf(i any) reflect.Type
		return CopySlice[reflect.TypeOf(vvItem)](val)
	case kind == reflect.Struct:
		return CopyStruct(val)
	case kind == reflect.Pointer:
		return CopyPointer(val)
	case kind == reflect.Map:
		return CopyMap(val)
	default:
		return val
	}

参考地址

medium.com/@aryehlevkl...

相关推荐
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
童先生9 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
幼儿园老大*10 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
架构师那点事儿15 小时前
golang 用unsafe 无所畏惧,但使用不得到会panic
架构·go·掘金技术征文
于顾而言1 天前
【笔记】Go Coding In Go Way
后端·go
qq_172805591 天前
GIN 反向代理功能
后端·golang·go
follycat2 天前
2024强网杯Proxy
网络·学习·网络安全·go
OT.Ter2 天前
【力扣打卡系列】单调栈
算法·leetcode·职场和发展·go·单调栈
探索云原生2 天前
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU
ai·云原生·kubernetes·go·gpu
OT.Ter2 天前
【力扣打卡系列】移动零(双指针)
算法·leetcode·职场和发展·go