go语言中的slice
slice有三个部分,第一部分,元素存哪里;第二部分,存多少个元素;第三部分,可以存多少个元素。
举个栗子:
声明一个整型slice
var ints []int
变量ints就有这样三个部分组成
slice的元素要存在一个连续的内存中,实际上就是个数组,data就是这个底层数组的起始地址,但是目前只分配了这个切片结构,还没有分配底层数组,所以data=nil,存储元素个数为0,容量也为0。
如果通过make的方式定义这个变量:
var ints []int = make([]int,2,5)
不仅会分配这三部分结构,还会开辟一段内存作为他的底层数组,这里,make会为ints开辟一段容纳5个整型元素的内存,还会把他们初始化为整型的默认值0
但是目前这个slice变量只存储了两个元素,所以data指向底层数组,len为2,cap为5
添加一个元素试试
ints=append(ints,1)
已经存储了两个,所以他是第三个,len修改为3
已经存储的元素是可以安全读写的,但是超出这个范围就是越界访问,会发生panic
ints[0] = 1
ints[3] = 2 //panic
再来个例子,这一次我们看看字符串类型的slice,但是不用make,来试试new,new一个slice变量
ps:=new([]string)
同样会分配这三部分结构,但他不负责底层数组的分配,所以data=nil,len=0,cap=0,new的返回值就是sllice结构的起始地址,所以ps他就是个地址,此时这个slice变量还没有底层数组
所以这样的操作是不允许的
(*ps)[0]="eggo"
那谁来给他分配底层数组呢?
append,通过append的方式添加元素,他就会给slice开辟底层数组
*ps=append(*ps,"eggo")
这里需要容纳一个字符串元素的数组,而字符串类型有两部分组成,一个是内容起始地址,指向字符串内容,还有一个是字节长度
再来看看和slice密切相关的底层数组
来看个例子,变量arr是容量为10的整型数组
arr:=[10]int{0,1,2,3,4,5,6,7,8,9}
注意,数组容量声明了,就不能变了,我们可以吧不同的slice关联到同一个数组,就像这样,他们会共用底层数组,看看s1和s2的具体结构就明白了
s1:=arr[1:4]
s2:=arr[7:]
s1的元素是arr索引1-4左闭右开,所以123算是添加到s1中了,但是容量算是从data这里开始到底层数组结束,共9个元素,同样的s2的元素从索引7开始直到结束,共3个元素,容量也是3,slice访问和修改的都是底层数组的元素, s1[3]就是访问越界了,可以通过修改s1:=arr[1:5]或者append添加元素来扩大可读写的区间范围。
此时如果再给s2添加元素会怎样?
s2=append(s2,10)
这个底层数组是不能用了,得开辟新数组,原来的元素要拷过来,还要添加新元素,元素个数改为4,容量为6
为什么容量为6呢,我们只添加了一个元素,为什么容量会从3变为6呢?这就要看sllice的扩容规则了
扩容规则第一步,预估扩容后的容量newCap,怎么预估,来看个例子
ints:=[]int{1,2}
ints=append(ints,3,4,5)
这里扩容前容量oldCap为2,添加三个元素,那至少得扩容到5吧,难道就是2+3=5?当然不是,预估也是有规则的:
如果扩容前容量翻倍还是小于所需最小容量,那么预估容量就等于所需最小容量
oldCap * 2 = 4 < 5
否则就要再细分,如果扩容前元素小于1024,则直接翻倍
oldLen < 1024---->newCap = oldCap *2
如果oldLen大于等于1024,那就先扩容个四分之一,也就是扩容到原来的1.25倍
所以这里ints添三个元素,扩容前容量oldCap*2 = 4 < 5 所以预估容量就是5
扩容规则第二步,预估容量只是预估的元素个数,这么多元素需要占多少内存呢?这就和元素类型挂钩了,用预估容量*元素类型大小得到的就是所需的内存大小,难道直接分配所需的内存就OK了吗?并不是,go语言中申请内存操作并不是直接和操作系统交互,而是和语言自身实现的内存管理模块,他会提前向操作系统申请一批内存,分成常用的规格管理起来,我们申请内存时,他会帮我们匹配到足够大,且最接近的内存规格。
这就是扩容规则第三步,将预估申请的内存,匹配到合适的内存规格,在之前的例子中,预估容量为5,在64位下int占8字节,就需要申请到5*8 = 40 bytes来存放扩容后的底层数组,而实际申请时,会匹配到48字节的内存规格(具体参考go语言内存管理,内存规格是16bytes叠加的,8,16,32,48,64,80,96...),在这个例子中,每个元素占8字节,所以最终扩容后的容量为48 / 8 = 6。
趁热打铁,再来个例子:
a是string类型的slice,64位下每个元素占16字节
a := []string{"my","name","is"}
a:=append(a,"aggo")
第一步
oldCap * 2 > 4 //原容量翻倍大于所需最小容量
oldLen < 1024 //扩容前元素大小小于1024
newCap = oldCap * 2 = 6 //所以预估容量为原容量直接翻倍
第二步
newCap * 16 = 96 bytes //计算预估容量所需内存大小
第三步
匹配内存规格96字节,故扩容后容量为6
注:本文总结自B站UP主幼麟实验室的视频,只为方便复习使用。