Go语言中的深拷贝和浅拷贝(图文并茂,附带实例)
在 Go 语言中,变量拷贝发生在执行数据处理的时候,目的是保留数据处理前的变量而重新定义新的变量。简单来说,就是将一个变量的数据复制并存放到另一个变量中。
变量拷贝分为深拷贝和浅拷贝,深浅拷贝的区别在于变量之间是否共用一个内存地址。
深拷贝是两个变量分别使用不同的内存地址存储相同的数据,如变量 a 的内存地址为 0xc000040240,存放数据为字符串“hello”;变量 b 通过变量 a 赋值,但变量 b 的内存地址为 0xc000040260,此时变量 a 和 b 在不同内存地址中,两者互不干扰,仅仅是存储了相同的数据。
浅拷贝是两个变量的内存地址相同,当其中一个变量修改数据时,另一个变量的数据也会随之变化,如变量 a 和变量 b 的内存地址为 0xc000040240,当变量 a 修改内存地址的数据后,变量 b 的数据也会随之变化,换句话说,浅拷贝好比一个人有两个名字。
Go 语言对不同数据类型的变量设置了对应的拷贝方式,不同的数据类型分为值类型变量和引用类型变量。
对值类型变量的说明如下:
对引用类型变量的说明如下:
为了更好地理解值类型数据、引用类型数据和深浅拷贝的关系,我们通过示例加以说明,代码如下:
1) 当变量为值类型变量的时候,如变量 s 为字符串类型,它仅分配了一个内存地址存储数据(即运行结果的 0xc000040240,%!p(string=hello)没有获取内存地址)。
2) 当变量 s 的值赋予变量 ss,变量 ss 拥有独立的内存地址,如 0xc000040270,变量 s 和 ss 是两个互不干扰的变量,说明值类型变量之间是通过深拷贝方式进行复制的。
3) 当变量为引用类型变量时,如变量 m 为集合 Map 类型,它具有两个不同的内存地址,如 0xc00007a3f0 和 0xc000006030,内存地址 0xc000006030 存储集合数据,而内存地址 0xc00007a3f0 作为变量 m 和集合数据的桥梁。
4) 当变量 m 的值赋予变量 mm 时,变量 mm 与变量 m 的内存地址相同,如内存地址 0xc00007a3f0,但两者的集合数据的内存地址不相同。
5) 当修改变量 mm 的集合数据时,尽管变量 m 和 mm 集合数据的内存地址不相同,但变量 m 的集合数据仍会随之变化。
如果将深浅拷贝、值类型和引用类型之间的关系通过图解表示,值类型与深拷贝的图解关系如下图所示:

图 1 值类型与深拷贝的图解关系
引用类型与浅拷贝的图解关系如下图所示:

图 2 引用类型与浅拷贝的图解关系
理解深浅拷贝、值类型和引用类型之间的关系之后,还需要掌握 Go 语言哪些数据类型是值类型;哪些数据类型是引用类型。在日常开发中,开发者常常会由于一时大意而忘记数据类型是值类型还是引用类型,导致程序出现错误而无法查明原因。
变量拷贝分为深拷贝和浅拷贝,深浅拷贝的区别在于变量之间是否共用一个内存地址。
深拷贝是两个变量分别使用不同的内存地址存储相同的数据,如变量 a 的内存地址为 0xc000040240,存放数据为字符串“hello”;变量 b 通过变量 a 赋值,但变量 b 的内存地址为 0xc000040260,此时变量 a 和 b 在不同内存地址中,两者互不干扰,仅仅是存储了相同的数据。
浅拷贝是两个变量的内存地址相同,当其中一个变量修改数据时,另一个变量的数据也会随之变化,如变量 a 和变量 b 的内存地址为 0xc000040240,当变量 a 修改内存地址的数据后,变量 b 的数据也会随之变化,换句话说,浅拷贝好比一个人有两个名字。
Go 语言对不同数据类型的变量设置了对应的拷贝方式,不同的数据类型分为值类型变量和引用类型变量。
对值类型变量的说明如下:
- 值类型变量是变量直接存储数据,内存通常在栈中分配;
- 值类型的数据类型:整型、布尔型、浮点型、字符串、数组和结构体等;
- 值类型变量的数据赋值到另一个变量都是深拷贝;
对引用类型变量的说明如下:
- 引用类型变量是变量存储一个内存地址,这个内存地址再存储数据,内存通常在堆上分配,通过 GC 回收;
- 引用类型的数据类型:指针、切片、集合、通道和接口等;
- 引用类型变量的数据赋值到另一个变量都是浅拷贝。
为了更好地理解值类型数据、引用类型数据和深浅拷贝的关系,我们通过示例加以说明,代码如下:
package main import "fmt" func main() { /* 值类型变量 */ s := "hello" fmt.Printf("变量s的内存地址:%p,变量值为:%v\n", s, s) fmt.Printf("变量s的内存地址:%p,变量值为:%v\n", &s, &s) // 将变量赋值给另一个变量,执行深拷贝方式 ss := s fmt.Printf("变量ss的内存地址:%p,变量值为:%v\n", ss, ss) fmt.Printf("变量ss的内存地址:%p,变量值为:%v\n", &ss, &ss) /* 引用类型变量 */ m := make(map[string]interface{}) m["name"] = "Tom" fmt.Printf("变量m的内存地址:%p,变量值为:%v\n", m, m) fmt.Printf("变量m的内存地址:%p,变量值为:%v\n", &m, &m) // 将变量赋值给另一个变量,执行浅拷贝方式 mm := m fmt.Printf("变量mm的内存地址:%p,变量值为:%v\n", mm, mm) fmt.Printf("变量mm的内存地址:%p,变量值为:%v\n", &mm, &mm) // 修改某个变量的值,另一个变量随之变化 mm["name"] = "Tim" fmt.Printf("变量m的内存地址:%p,变量值为:%v\n", m, m) fmt.Printf("变量m的内存地址:%p,变量值为:%v\n", &m, &m) fmt.Printf("变量mm的内存地址:%p,变量值为:%v\n", mm, mm) fmt.Printf("变量mm的内存地址:%p,变量值为:%v\n", &mm, &mm) }运行上述代码,运行结果为:
变量 s的内存地址: %!p(string=hello),变量值为: hello
变量 s的内存地址: 0xc00000402240,变量值为: 0xc00000402240
变量 ss的内存地址: %!p(string=hello),变量值为: hello
变量 ss的内存地址: 0xc0000040270,变量值为: 0xc0000040270
变量 m的内存地址: 0xc000007a3f0,变量值为: map[name:Tom]
变量 m的内存地址: 0xc000006030,变量值为: &map[name:Tom]
变量 mm的内存地址: 0xc000007a3f0,变量值为: map[name:Tom]
变量 mm的内存地址: 0xc000006038,变量值为: &map[name:Tom]
变量 m的内存地址: 0xc000007a3f0,变量值为: map[name:Tim]
变量 m的内存地址: 0xc000006030,变量值为: &map[name:Tim]
变量 mm的内存地址: 0xc000007a3f0,变量值为: map[name:Tim]
变量 mm的内存地址: 0xc000006038,变量值为: &map[name:Tim]
1) 当变量为值类型变量的时候,如变量 s 为字符串类型,它仅分配了一个内存地址存储数据(即运行结果的 0xc000040240,%!p(string=hello)没有获取内存地址)。
2) 当变量 s 的值赋予变量 ss,变量 ss 拥有独立的内存地址,如 0xc000040270,变量 s 和 ss 是两个互不干扰的变量,说明值类型变量之间是通过深拷贝方式进行复制的。
3) 当变量为引用类型变量时,如变量 m 为集合 Map 类型,它具有两个不同的内存地址,如 0xc00007a3f0 和 0xc000006030,内存地址 0xc000006030 存储集合数据,而内存地址 0xc00007a3f0 作为变量 m 和集合数据的桥梁。
4) 当变量 m 的值赋予变量 mm 时,变量 mm 与变量 m 的内存地址相同,如内存地址 0xc00007a3f0,但两者的集合数据的内存地址不相同。
5) 当修改变量 mm 的集合数据时,尽管变量 m 和 mm 集合数据的内存地址不相同,但变量 m 的集合数据仍会随之变化。
如果将深浅拷贝、值类型和引用类型之间的关系通过图解表示,值类型与深拷贝的图解关系如下图所示:

图 1 值类型与深拷贝的图解关系
引用类型与浅拷贝的图解关系如下图所示:

图 2 引用类型与浅拷贝的图解关系
理解深浅拷贝、值类型和引用类型之间的关系之后,还需要掌握 Go 语言哪些数据类型是值类型;哪些数据类型是引用类型。在日常开发中,开发者常常会由于一时大意而忘记数据类型是值类型还是引用类型,导致程序出现错误而无法查明原因。