前言
go语言切片是一种重要的数据类型,在工作中我们会经常遇到。本文以切片中常踩的坑入手,深入浅出的分析切片的底层原理。
常见的坑
1.切片的初始化方式不同
make([]string, 0, 14) 和 make([]string, 14) 的区别在于切片的初始长度和容量。
- make([]string, 0, 14):
创建了一个初始长度为 0,容量为 14 的字符串切片。这意味着切片的底层数组具有能容纳 14 个字符串元素的空间,但切片本身当前没有包含任何元素。在这种情况下,切片的长度为 0,容量为 14。 - make([]string, 14):
创建了一个初始长度为 14,容量也为 14 的字符串切片。这意味着切片的底层数组包含了 14 个字符串元素的空间,并且切片本身已经包含了 14 个零值(对于字符串类型,零值为 “”)。
所以当我们在用第二种方式创建切片之后,我们只append了一个元素,最后切片的大小却不是1,而是5+1。
场景:我从数据库读取每一条记录,然后append到切片中,然后根据切片里的数据量分配给一个map(map的key为主机hostname,value为分配的数据),最终5条记录分配完应该是2,2,1.但最终却是5,5,5。
2.切片的访问范围
func TestSlice2(t *testing.T) {
slice1 := make([]string, 0, 5)
slice1 = append(slice1, "111")
fmt.Println(fmt.Sprintf("slice1 len is %v cap is %v", len(slice1), cap(slice1)))
fmt.Println(slice1[0])
fmt.Println(slice1[1])
}
可以看到切片的访问范围是它的长度。
3.切片的截取slice:=slice[:3]
func TestSliceCap(t *testing.T) {
slice1 := make([]string, 0, 5)
slice1 = append(slice1, "111")
slice1 = append(slice1, "222")
fmt.Println(fmt.Sprintf("slice1 len is %v cap is %v", len(slice1), cap(slice1)))
slice2 := slice1[:1]
fmt.Println(fmt.Sprintf("slice2 len is %v cap is %v", len(slice2), cap(slice2)))
slice3 := slice1[:3]
fmt.Println(fmt.Sprintf("slice3 len is %v cap is %v", len(slice3), cap(slice3)))
slice4 := slice1[:6]
fmt.Println(fmt.Sprintf("slice4 len is %v cap is %v", len(slice4), cap(slice4)))
}
从上述test中可以总结出:
1.当截取的长度小于等于原切片的长度时,新切片的len为截取的值,cap为原切片的cap。
2.当截取的长度大于原切片的长度时且小于原切片的容量时,新切片的len为截取的值,cap为原切片的cap。且新切片多余的值为默认值。如slice3[0]=“111”,slice3[1]=“222”,slice3[2]=“”。
3.当截取的长度大于原切片的容量时,会直接报panic。
4.切片的append
案例1:
func TestSlice4(t *testing.T) {
arr := make([]int, 3, 5)
brr := append(arr, 9)
fmt.Println(fmt.Sprintf("arr len is %v cap is %v", len(arr), cap(arr)))
fmt.Println(fmt.Sprintf("brr len is %v cap is %v", len(brr), cap(brr)))
brr[0] = 1
for _, v := range arr {
fmt.Println(v)
}
}
最终arr和brr的长度和容量分别是多少呢?arr和brr的切片输出的是什么呢?
arr len is 3 cap is 5
brr len is 4 cap is 5
1
0
0
-----
1
0
0
9
可以看到我们更改了brr[0]的值,arr[0]及brr[0]的值都变为了1。且len(arr)=3,len(brr)=4。
当用append在切片中新增一个元素时,会重新分配一个地址。brr的地址和arr的地址不同,但是brr和arr的第一个元素指针都指向共同的底层数组,区别就是brr的len=4,而arr的len=3,所以arr只能访问前三个元素。
理清切片append的本质之后,下面代码会输出什么呢?我们给arr增加一个元素,那brr[3]会改变吗?
案例2:
func TestSlice4(t *testing.T) {
arr := make([]int, 3, 5)
brr := append(arr, 9)
fmt.Println(fmt.Sprintf("arr len is %v cap is %v", len(arr), cap(arr)))
fmt.Println(fmt.Sprintf("brr len is %v cap is %v", len(brr), cap(brr)))
brr[0] = 1
arr = append(arr, 4)
fmt.Println(arr)
fmt.Println("-----")
fmt.Println(brr)
}
当然,因为arr和brr共有的是一块内存地址,所以arr的append相当于是改变了数组的第四个值。如下图所示
继续思考,如果是这种情况呢?
案例3:
func TestSlice6(t *testing.T) {
arr := make([]int, 3, 3)
brr := append(arr, 9)
fmt.Println(fmt.Sprintf("arr len is %v cap is %v", len(arr), cap(arr)))
fmt.Println(fmt.Sprintf("brr len is %v cap is %v", len(brr), cap(brr)))
brr[0] = 1
arr = append(arr, 4)
fmt.Println(arr)
fmt.Println("-----")
fmt.Println(brr)
}
可以看到,arr,brr的结果互不影响了。
小结
1. 底层数组被共享的情况(容量未耗尽):
- 起点:arr := make([]int, 3, 5) 创建了一个长度为 3、容量为 5 的切片。
- 追加操作:brr := append(arr, 9) 使 brr 和 arr 共享同一个底层数组,因为 arr 的容量未耗尽(容量为 5),brr 的长度为 4,容量仍为 5。
- 修改操作:brr[0] = 1 修改了 brr,由于 arr 和 brr 共享底层数组,arr[0] 也被修改。
- 再次追加操作:arr = append(arr, 4) 由于 arr 的容量足够,直接在同一个底层数组上追加了 4,结果使得 arr 和 brr 都变为 [1, 0, 0, 4]。
- 结果:arr 和 brr 最终都变为 [1, 0, 0, 4],因为两者共享底层数组。
底层数组未共享的情况(容量耗尽后触发了重新分配): - 起点:同样从 arr := make([]int, 3, 3) 创建一个长度为 3、容量为 3的切片开始。
- 追加操作:brr := append(arr, 9) ,此时 brr再次进行 append 操作并导致容量不足(例如再增加多个元素),会触发底层数组重新分配。此时,brr会拥有一个新的底层数组。
- 修改操作:brr[0] =1及arr=append(arr,4)底层是两块不同的内存地址,操作互不影响
- 结果:arr 和 brr 最终值不同,因为 brr 触发了底层数组的重新分配,两个切片不再共享同一个底层数组。
主要区别:
共享底层数组时:arr 和 brr 的修改和追加操作会互相影响,因为它们引用同一个底层数组。
不共享底层数组时(因为追加导致容量耗尽而重新分配):arr 和 brr 的修改和追加操作不会互相影响,因为它们使用不同的底层数组。
总结`
通过实际项目中踩过的坑,来探究一下切片操作的底层原理。其实切片本质是就是一个动态的数组,它提供了对底层数组的一个引用,同时能够动态调整长度。切片的本质是一个对底层数组的抽象,它包含三个字段:
- 指向底层数组的指针:指向切片所引用的数组的起始位置。
- 切片的长度(len):切片当前所包含的元素个数。(决定了切片访问的范围)
- 切片的容量(cap):从切片起始位置到底层数组末尾所能容纳的元素个数。(根据容量决定是否重分配)
喜欢的小伙伴可以点赞加关注哈,后续将会持续分享一些go语言的知识点及一些中间件的知识。