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...

相关推荐
研究司马懿14 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰1 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘2 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤2 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto4 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo