什么是切片
Go语言切片是对数组的抽象。它具有动态数组的功能,可以追加元素使切片的容量增大。
但是,需要说明的是,切片的底层还是用数组实现的,且它不是数组或数组指针。它自身是一个结构体,通过内部指针和相关属性引用数组片段,达到动态数组的效果。
可是只是理解概念并不能满足我们对切片实现原理的好奇心,它真的没有使用像链表类似的数据结构来实现的吗?
切片实现的底层原理
在Go语言中传递数组就是对数组值得拷贝,对于元素类型长度较大或者元素个数较多得数组,直接将数组当作参数进行传递,意味着要把整个数组进行拷贝一次,有着不小的性能损耗。
解决这一问题的办法是直接把数组指针当作形参,只传递数组首元素的地址即可。
切片大概也是以这样的方法进行避免这一问题的,但对于前者的作用更进一步。
切片的结构体
type slice struct {
array unsafe.Pointer
len int
cap int
}
array :只想下层数组的指针。
len:切片的长度,即内置函数len()返回的值
cap: 切片的最大容量,即内置函数cap()返回的值, cap>=len
定义切片的几种方式
一个抽象的底层原理往往可以从它的定义方式来表现出来。
将数组进行切片
定义一个数组
a := [10]int {11,12,13,14,15,16,17,18,19,20}
定义数组的切片
s := a[3:7]
此时,数组和切片的内存布局就是这样的:
我们可以看得出来,其实切片时引用了数组的某一片段。array指向的时是底层数组的第一个元素。
切片的切片
同时,我们也可以对切片进行切片
基于s定义一个新的切片s1
s1 := s[1:3]
定义切片时超过底层数组的容量
就是在定义s1时的low:high超过s的cap大小,Go会怎么处理呢?
此时,会自动为切片建立一个底层数组,内存布局如下:
需要注意的是,如果想对底层数组进行切片,low:high的值不能超过底层数组的范围
切片动态扩容原理
切片很奇妙的一个地方就是可以通过append()方法对切片进行动态扩容。
实际上它的原理很简单,就是append会根据切片的需要,在当前底层数组的容量无法满足的情况下,重新创建一个新的更大容量的数组,把旧的数组数据复制到新的数组中。
func main() {
arr := [3]int{1, 2, 3}
/*
记录当前数组的地址
*/
s := arr[:]
//因为我们知道切片和数组的地址是一样的,所以现在记录切片还是数组都是可以的
//为什么要将数组转化为切片?
//因为数组无法动态扩容,只有切片可以
var p1 *[3]int
p1 = &arr
//进行append
s = append(s, 1)
//记录append后的数组地址
var p2 *[]int
p2 = &s
fmt.Println(p1, p2)
fmt.Println(&p1, &p2)
}
我们看一下输出结果
元素的确追加了,并且两个切片的地址是不同的。在append扩容之后,底层确实创建了一个新的切片进行拷贝。
总结
切片的存在确实让我们对数组有了更加灵活的操作。在扩容方面,拷贝后的旧数组会被gc垃圾回收,也避免了多余的内存占用,但是拷贝的过程所消耗的时间是无法避免的。