go语言切片你了解吗?常见的坑


前言

go语言切片是一种重要的数据类型,在工作中我们会经常遇到。本文以切片中常踩的坑入手,深入浅出的分析切片的底层原理。

常见的坑

1.切片的初始化方式不同

make([]string, 0, 14) 和 make([]string, 14) 的区别在于切片的初始长度和容量。

  1. make([]string, 0, 14):
    创建了一个初始长度为 0,容量为 14 的字符串切片。这意味着切片的底层数组具有能容纳 14 个字符串元素的空间,但切片本身当前没有包含任何元素。在这种情况下,切片的长度为 0,容量为 14。
  2. 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语言的知识点及一些中间件的知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花箫乱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值