一篇文章让你彻底搞懂Go语言切片底层原理

1. 切片和数组的底层关系

Go语言切片的数据结构是一个结构体:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

Go语言中切片的内部结构包含地址、大小和容量。将数组比喻成一个蛋糕,那么切片就是需要切的那一块,而那一块的的大小就是切片的大小,而容量可以理解为装这一块蛋糕的袋子的大小。通过切片,我们可以快速地对数组进行操作。

从数组或切片中获取新切片

从数组中获取新切片,代码如下:
var a  = [3]int{1, 2, 3}
fmt.Println(a, a[1:2])

a是一个被初始化为长度为3,值为{1,2,3}的一维数组。使用a[1:2]可以生成一个新的切片:

[1 2 3]  [2]
从数组中获取原切片,代码如下:
a := []int{1, 2, 3}
fmt.Println(a[:])

对a使用a[:]操作后,生成的切片与原数组内容一致。

清空切片
a := []int{1, 2, 3}
fmt.Println(a[0:0])

对a使用a[0:0]操作后,切片大小为0,相当于清空了切片。

综上,我们发现获取切片,实际上是对底层数组的某一片段拿出来进行操作。非常类似于C语言的指针,可以通过指针运算,来达到类似切片的目的,但是存在野指针的风险。
而切片在指针的基础上增加了大小,使用中不允许对切片的内部地址和大小进行手动调整。因此比指针更加安全、更加强大。

简单来说,切片在内部对指针进行了限制和管理,从而实现更加安全且快速地对数据集合进行操作。

2. 切片的扩容机制

使用make函数构造切片

若需要动态地构建一个切片,则需要使用make函数:

make( []Type, size, cap )

size指这个切片的实际大小;cap指的是预分配的内存空间大小。
make函数构造切片的过程中是一定进行了内存分配的操作

扩容

当对切片进行动态地添加元素时,若切片大小超出容量,容量会以2的倍数进行扩容。
我们看这样一个案例:

silce := make([]int, 0)
	for i := 0; i < 10; i++ {
		silce = append(silce, i+1)
		fmt.Printf("len:%d cap:%d p:%p\n", len(silce), cap(silce), silce)
	}

可以发现:切片的大小和容量的关系只有在切片的大小超过切片的容量时,才会触发切片容量的扩容,且每次扩容都是2倍扩容。

len:1 cap:1 p:0xc00000a0c8
len:2 cap:2 p:0xc00000a110
len:3 cap:4 p:0xc000012220
len:4 cap:4 p:0xc000012220
len:5 cap:8 p:0xc0000183c0
len:6 cap:8 p:0xc0000183c0
len:7 cap:8 p:0xc0000183c0
len:8 cap:8 p:0xc0000183c0
len:9 cap:16 p:0xc000100080
len:10 cap:16 p:0xc000100080

观察每次扩容,切片的地址都会进行改变,这是为什么呢?

我们在上文讲"切片与数组"的关系时,分析过:切片的本质是一种"安全的指针"。而底层数组的内存大小被分配结束后是无法进行扩容的(本质上是顺序表)。因此,若要进行扩容,那么只能创建一个新的数组(Go内部规定容量为原先的2倍),然后将原数组的数据转移到新数组内,并让切片的指针重新指向新的数组。

因此,每次扩容都会进行一次“搬家”,而搬家后,家的指针自然要改变到新家。

3. 切片在函数中的传参

我们看下面这样一个案例:

func test(target *[]int) {
	fmt.Printf("process before:address of slice %p \n", *target)
	*target = append(*target, 1)
	fmt.Println(*target)
	fmt.Printf("process after:address of slice %p \n", *target)
}
func test2(target []int) {
	fmt.Printf("process before:address of slice %p \n", target)
	target = append(target, 1)
	fmt.Println(target)
	fmt.Printf("process after:address of slice %p \n", target)
}
 
func main () {
	var tt = []int{4,5}
	fmt.Printf("init:address of slice %p \n", tt)
	test(&tt)
	fmt.Println(tt)
	fmt.Printf("after:address of slice %p \n", tt)
	fmt.Println("--------------")
	
	var tt2 = []int{4,5}
	fmt.Printf("init:address of slice %p \n", tt)
	test2(tt2)
	fmt.Println(tt2)
	fmt.Printf("after:address of slice %p \n", tt2)
}

输出结果为:

init:address of slice 0xc000016060 
process before:address of slice 0xc000016060 
[4 5 1]
process after:address of slice 0xc000014060 
[4 5 1]
after:address of slice 0xc000014060 
--------------
init:address of slice 0xc000014060 
process before:address of slice 0xc0000160a0 
[4 5 1]
process after:address of slice 0xc000014080 
[4 5]
after:address of slice 0xc0000160a0 

可以看出来,切片作为函数参数进行传参。它实际上传入的是一个切片结构体副本,但这两个切片都指向同一底层数组,通过append操作后,切片重新生成,修改的值无法向函数外传递。(两个切片的底层数组是相同的,不同的是len和cap)
而将切片的指针作为参数传入,那么操作的对象就一直为同一个结构体和底层数组。

4. 小试牛刀

我们看这样一段代码,来复习我们对切片的底层理解:

diySlice := make([]int, 0, 2)
	diySlice = append(diySlice, 8)

	//观察diySlice3
	diySlice3 := append(diySlice, 1)
	//diySlice 变化
	//问题1:查看输出切片的变化,为什么和直接输出结果不一样
	fmt.Println("diySlice内容下标", diySlice[0:2])
	//查看输出切片的变化
	fmt.Println("diySlice 内容", diySlice)
	//查看长度和容量
	fmt.Printf("diySlice-->容量%d 长度%d\n", cap(diySlice), len(diySlice))

	fmt.Println("diySlice3 内容", diySlice3)
	fmt.Printf("diySlice3-->容量%d 长度%d\n", cap(diySlice3), len(diySlice3))

	//问题2:观察diySlice2
	diySlice2 := append(diySlice, 1, 2)
	fmt.Println("diySlice2 内容", diySlice2)
	fmt.Printf("diySlice2-->容量%d 长度%d\n", cap(diySlice2), len(diySlice2))
  • 问题1:为什么diySilce[0:2]diySilce的输出结果不同?
    首先明确,diySilce的底层数组是不变的,也就是“蛋糕”只有那么一块。
    那么输出结果的不同,取决于切片的大小diySilce[0:2]的切片大小为2,而diySilce只有第一次进行的append操作返回的新切片重新返回给了diySilce本身,此时len(diySilce)为1
    而第二次进行的append操作,返回的切片给了diySilce3。所以此时的len(diySilce3)为2,len(diySilce)依然为1。
    由于它们指向同一个底层数组,所以两个append操作对数组是有效的。输出结果的不同,只是len将切片管住了。
    那么答案显而易见:
    底层数组为:{8,1}。diySilce3[0:2]取数组中0~1下标的元素,而diySilce大小为1,只能取到数组的第一个元素。
  • 问题2:观察diySilce2发现切片的容量变为4,比原来多了1倍。
    这就涉及到切片的扩容问题。我们分别打印一下diySilcediySilce2diySilce3的地址:
diySilce:0xc00000a0e0
diySilce3:0xc00000a0e0
diySilce2:0xc000012220

观察可以发现,切片在第三次进行append操作后,切片的大小已经超过切片的容量。所以只能创建一个新的底层数组来存储元素,新数组的大小为原数组大小的2倍数。

总结

相信认真看完本篇文章后,你会对切片的底层原理有了更加深入的理解。若有其他问题,可在评论区询问,bye~。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值