一个问题让你搞清Golang数组和切片的变量复制

切片slice和数组array是golang中常见且重要的数据类型。一言以蔽之,从使用角度看来,数组是有固定长度的特殊切片;而切片是一个可以自动扩容的不定长数组。

但是两种数据类型在底层实现上又有着本质的区别。我们知道在golang中变量分为值类型变量和引用类型变量。其中 string、int、float、bool和 array等基础类型属于值类型变量,而 slice、map、chan 等则属于引用类变量。

在对象拷贝时,golang默认采用值拷贝的策略,即深度拷贝策略;但是对slice、map、chan 等类型,由于其本身属于引用类型,变量本身存储的就是指针数据,所以实际为浅拷贝。因此数组和分片在拷贝后的修改对于原来变量的影响会有所区别。下面我们用三个例子来说明这种区别

1、数组拷贝

 

golang

代码解读

复制代码

func main() { a := [3]int{1, 2, 3} b := a c := &a b[0] = 10 c[0] = 5 fmt.Printf("a = %v,ptr = %p\n", a, &a) fmt.Printf("b = %v,ptr = %p\n", b, &b) fmt.Printf("c = %v,ptr = %p\n", c, c) }

测试输出为:

 

text

代码解读

复制代码

a = [5 2 3],ptr = 0x1400011a018 b = [10 2 3],ptr = 0x1400011a030 c = &[5 2 3],ptr = 0x1400011a018

通过上面的例子,我们可以发现, 数组对象拷贝后会生成两个不相关的数组变量,分别指向不同的地址空间,修改某一个变量,另一个变量不会受到影响,但是当我们通过变量c来拷贝a变量的指针时,通过修改变量c,是可以影响变量a的,因为两个对象指向同一块地址空间。

2、切片拷贝1

 

golang

代码解读

复制代码

func main() { a := []int{1, 2, 3} b := a[:2] fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b[0] = 10 fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b = append(b, 4) fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b = append(b, 5) fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b[0] = 20 fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) }

测试输出为:

 

text

代码解读

复制代码

a = [1 2 3],cap = 3,len= 3, 底层数组地址 = 0x1400001a120, 变量地址 = 0x1400000c0c0 b = [1 2],cap = 3,len= 2, 底层数组地址 = 0x1400001a120, 变量地址 = 0x1400000c0d8 a = [10 2 3],cap = 3,len= 3, 底层数组地址 = 0x1400001a120, 变量地址 = 0x1400000c0c0 b = [10 2],cap = 3,len= 2, 底层数组地址 = 0x1400001a120, 变量地址 = 0x1400000c0d8 a = [10 2 4],cap = 3,len= 3, 底层数组地址 = 0x1400001a120, 变量地址 = 0x1400000c0c0 b = [10 2 4],cap = 3,len= 3, 底层数组地址 = 0x1400001a120, 变量地址 = 0x1400000c0d8 a = [10 2 4],cap = 3,len= 3, 底层数组地址 = 0x1400001a120, 变量地址 = 0x1400000c0c0 b = [10 2 4 5],cap = 6,len= 4, 底层数组地址 = 0x14000016210, 变量地址 = 0x1400000c0d8 a = [10 2 4],cap = 3,len= 3, 底层数组地址 = 0x1400001a120, 变量地址 = 0x1400000c0c0 b = [20 2 4 5],cap = 6,len= 4, 底层数组地址 = 0x14000016210, 变量地址 = 0x1400000c0d8

通过上面的例子可以发现,切片对象拷贝后,会生成两个变量,变量地址指向不同的地址空间,从a和b 分别具有不同的 len 值,也可以确认a和b对象本身是两个完全独立的对象。但是两个变量指向的底层数组地址一致,说明切片对象内容的拷贝是浅拷贝。因此当我们操作b[0]的数值时,a 切片的值也会受到影响。

当我们第一次针对b执行append(b, 4)操作时,由于其b变量的cap空间足够,因此不会重现生成新的底层数组对象,此时与a仍然共用同一个底层数据,只不过会将4覆盖原来b[2]的值,因此a[2]的值也会受到影响。

当我们第二次针对b执行append(b, 5)操作时,由于其b变量的cap空间已经被全部占用,因此触发切片扩容,需要重现申请底层数组空间,此时我们可以看到,b变量本身的地址不变,但是其指向的底层数组地址发生了变化。因此此时我们在修改b[0]时,a[0]的值不会受到影响。

切片扩容的原理及源码实现可以参考上一篇文章golang数组和切片的区别

3、切片拷贝2

 

golang

代码解读

复制代码

func main() { a := []int{1, 2, 3} b := a[1:] fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b[0] = 10 fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b = append(b, 4) fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b[0] = 20 fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) }

测试输出为:

 

text

代码解读

复制代码

a = [1 2 3],cap = 3,len= 3, 底层数组地址 = 0x1400011a018, 变量地址 = 0x1400012c0a8 b = [2 3],cap = 2,len= 2, 底层数组地址 = 0x1400011a020, 变量地址 = 0x1400012c0c0 a = [1 10 3],cap = 3,len= 3, 底层数组地址 = 0x1400011a018, 变量地址 = 0x1400012c0a8 b = [10 3],cap = 2,len= 2, 底层数组地址 = 0x1400011a020, 变量地址 = 0x1400012c0c0 a = [1 10 3],cap = 3,len= 3, 底层数组地址 = 0x1400011a018, 变量地址 = 0x1400012c0a8 b = [10 3 4],cap = 4,len= 3, 底层数组地址 = 0x1400014e040, 变量地址 = 0x1400012c0c0 a = [1 10 3],cap = 3,len= 3, 底层数组地址 = 0x1400011a018, 变量地址 = 0x1400012c0a8 b = [20 3 4],cap = 4,len= 3, 底层数组地址 = 0x1400014e040, 变量地址 = 0x1400012c0c0

通过上面的例子可以发现,切片对象拷贝后,由于b取的是a数组的后两个元素,而数组的空间是连续的,因此b切片的cap变为2,同时b的底层数据地址刚好指向a切片底层数组的第二个元素地址。由于我们的数组类型为int类型,系统为64位,每个元素占用8字节,由于数组地址连续,b的底层数组地址 正好等于 切片a 底层数组地址 0x1400011a018 + 8,即b切片第一个元素地址正对切片底层数组的第二个元素。此时我们修改 b[0] = 10 值,则a[1]也会同步被修改为10。

当我们针对b执行append(b, 4)操作时,由于其b变量的cap空间已经被全部占用,因此触发切片扩容,需要重现申请底层数组空间,此时我们可以看到,b切片指向的底层数组地址发生了变化。因此此时我们再修改b[0]时,a[1]的值不会受到影响。

4、总结

相信通过上面三个测试用例,可以很清楚的说明golang中数组和切片变量的对象复制区别。我们可以总结为以下几个要点:

  • 数组变量为值复制,复制后变量之间独立。除非指定为变量指针传递。
  • 切片变量为指针复制,复制后变量操作是否会影响原变量,取决于该才操作是否会触发切片的扩容。

请思考以下测试用例的输出结果:

 

golang

代码解读

复制代码

func main() { a := []int{1, 2, 3, 4} b := a[1:3] fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b[0] = 10 fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b = append(b, 5) fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b[0] = 20 fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b = append(b, 6) fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) b[0] = 30 fmt.Printf("a = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", a, cap(a), len(a), a, &a) fmt.Printf("b = %+v,cap = %d,len= %d, 底层数组地址 = %p, 变量地址 = %p\n", b, cap(b), len(b), b, &b) }

Golang数组切片是两种不同的数据类型,用于存储相同数据类型的容器。数组的长度是固定的,而切片的长度是可变的。在日常应用中,切片的使用更为普遍。 数组在声明时需要指定长度,并且在初始化时必须提供相同长度的元素。例如,`a := int{1, 2, 3}`就是一个长度为3的整数数组数组的长度一旦确定后就不能更改。 切片是基于数组的引用类型。它不需要指定固定的长度,并且可以根据需要动态扩展或缩小。切片包装着底层数组,通过指定起始索引结束索引来指定子集。例如,`b := a[:]`就是一个切片,它包含了数组a的所有元素。 数组适用于需要固定长度的场景,而切片适用于长度可变的情况。在实际应用中,切片更常用,因为它提供了更大的灵活性便利性。 总结: - 数组是长度固定的容器,切片是长度可变的容器; - 数组在声明时需要指定长度,切片则不需要; - 数组的长度一旦确定后就不能更改,而切片可以根据需要动态扩展或缩小; - 切片是基于数组的引用类型,可以通过指定起始索引结束索引来指定子集。 参考资料: Golang中的「数组切片」都是存储同一数据类型的容器,只不过Golang中的数组长度是固定的,而切片的长度是可变化的。 切片是引用类型,切片包装的数组称为该切片的底层数组。我们来看一段代码://a是一个数组,注意数组一个固定长度的,初始化时候必须要指定长度,不指定长度的话就是切片了 a := int{1, 2, 3} //b是数组,是a...。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值