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倍。
这就涉及到切片的扩容问题。我们分别打印一下diySilce
、diySilce2
、diySilce3
的地址:
diySilce:0xc00000a0e0
diySilce3:0xc00000a0e0
diySilce2:0xc000012220
观察可以发现,切片在第三次进行append
操作后,切片的大小已经超过切片的容量。所以只能创建一个新的底层数组来存储元素,新数组的大小为原数组大小的2倍数。
总结
相信认真看完本篇文章后,你会对切片的底层原理有了更加深入的理解。若有其他问题,可在评论区询问,bye~。