Go 语言中的数组长度不可变,这在使用的过程中带来了不便,为了弥补这个缺陷,Go 语言提供了一种新的数据类型——切片,有人也把它称为动态数组,切片的长度是可变的,在往切片中添加元素时,可能会因为原有切片容量不足,而重新开辟一块空间存储数据。
2、切片的数据结构
在 reflect 包中,我们可以看到 SliceHeader(切片头)数据结构,它就代表了切片的数据结构,在下面的代码中,Len 代表切片中目前存储的元素数目,Cap 代表切片底层数组的长度。每次我们往切片中添加元素时,切片的 Cap 如果尚且大于 Len 的话,则还在原内存空间中扩展,但是如果切片的 Cap 小于 Len 的话,则会重新在内存中开辟出一块空间供切片存储数据使用,切片扩容稍后介绍。
type SliceHeader struct{
Data uintptr
Len int
Cap int}
3、切片的声明与初始化
切片的声明方式是:var slice []T,其中 T 是切片元素的类型,与数组不同的是,切片在声明的时候不要在方括号 [] 中写入切片的长度,如果在方括号 [] 中写入数字,则就不是切片,而是数组了。切片还可以基于现有的切片或数组生成,方式是 slice := array[n:m],其中 array 可能是切片,也可能是数组,并且 {n >= 0 && n < m && m <= len(array)}。
var a =[]int{1,2,3,4}
b := a[:2]//b->{1, 2}
b[0]=10//b->{10, 2} and a->{10, 2, 3, 4}//注意这里 a 的 Len 和 Cap 都是4,b 的 Len 和 Cap 分别是 2 和 4//前面说过 Cap 代表切片底层数组的长度,从这里 b 的 Cap 和 a 的 Cap 一样,//可以反映出新切片复用原来切片的底层数组
但是假如我对新切片进行扩容了呢?注意,只是增加了新切片的 Len,而 Cap 没有增加,实际上不算是扩容。看下面代码,真正的扩容在下一节讲解。
var a =[]int{1,2,3,4}
b := a[:2]//b->{1, 2}
b =append(b,[]int{5,6}...)//b->{1, 2, 5, 6} and a->{1, 2, 5, 6}//尽管 b 的 Len 变成了 4,但是 Cap 依然是 4,没有增加,所以没有扩容,//也就是说 b 没有开辟新的空间去存储底层数组,那么这时对 b 进行修改,//a 的底层数组对应位置数据也就被修改了
4.1.1、通过指针偏移获取元素
虽然 Go 语言为了安全,对指针做了诸多限制,比如在 C 语言中可对指针进行运算,从而获取到其他内存位置存储的数据(这么做很危险,可能会破坏系统变量),但是在 Go 语言中,禁止了对指针进行运算这一操作。然而我们还是通过一些手段实现对指针的运算操作,只是步骤稍微麻烦了一些。大致可分为六个步骤,看下面代码。
Go 语言中的切片扩容机制,与切片的数据类型、原本切片的容量、所需要的容量都有关系,比较复杂。对于常见数据类型,在元素数量较少时,大致可以认为扩容是按照翻倍进行的,下面是 runtime 包里 slice.go 切片扩容的部分底层源码:
funcgrowslice(et *_type, old slice,capint) slice {//省略部分代码
newcap := old.cap
doublecap := newcap + newcap
ifcap> doublecap {
newcap =cap}else{if old.len<1024{//当原本切片长度小于 1024 时,扩容时容量直接翻倍
newcap = doublecap
}else{//否则当原本切片长度大于 1024 时,容量会在不溢出且小于扩容目标情况下每次增加 25%// Check 0 < newcap to detect overflow 检查不会溢出// and prevent an infinite loop. 防止无限循环for0< newcap && newcap <cap{
newcap += newcap /4//每次增加25%}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <=0{
newcap =cap}}}//省略部分代码}
切片扩容会开辟一块新的空间存储切片。
var a []int=[]int{1,2,3,4}//a.len=4, a.cap=4, &a=0xc0000044a0
fmt.Printf("cap:%d, len:%d, addr:%p\n",cap(a),len(a),&a)
a =append(a,[]int{0,4,3,2,1}...)//a.len=9, a.cap=10, &a=0xc000096440
fmt.Printf("cap:%d, len:%d, addr:%p\n",cap(a),len(a),&a)
4.2.1、对切片进行扩容的一点建议
前面我们说过,只有当 Cap 增加时,切片才算发生了扩容,但是在写代码时,我们有时并不知道切片有没有扩容,比如我们通过切片的方式得到的一个新切片,我们要在新切片上添加数据,如果往新切片中添加数据,并没有发生扩容,那么就会修改原本切片中存储的数据,为了避免这种意想不到的事情发生,对切片进行扩容时,建议使用 copy 方式,得到一个重新分配内存空间的切片后,再进行扩容。
var srcSlice =[]int{1,2,3,4}var dstSlice =make([]int,4,8)//注意:这里设置的 len 必须大于或者等于srcSlice 的长度才能完整复制 srcSlice 到 dstSlice
res :=copy(dstSlice, srcSlice)