切片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) }