1. 切片和数组的底层关系
Go语言切片的数据结构是一个结构体:
type slice struct {array unsafe.Pointerlen intcap 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倍),然后将原数组的数据转移到新数组内,并让切片的指针重新指向新的数组。
因此,每次扩容都会进行一次“搬家”,而搬家后,家的指针自然要改变到新家。
未完待续