前引
观看B站博主小徐先生1212的《Go语言切片slice技术原理与应用实战》后
数据结构
Go
type slice struct{
//指向起点的地址
array unsafe.Pointer
//切片长度
len int
// 切片容量
cap int
}
首先slice,相当于其他语言中数组的一个定位,但是拥有go语言的特性,这里slice是可以扩容操作的。
这里unsafepointer为什么是unsafe,简单了解了一下,反正就是可以转换指针类型,查了点资料

1.array unsafe.Pointer
一个指针,指向slice开头对应的一个内存地址,这里slice数据值是连续的,获得开始地址,根据index就可以通过偏移地址定位每一个元素。
2.len
slice实际储存元素的一个长度
3.cap
slice,可以物理上储存的一个最大容量,通常len<=cap
初始化
先给出一个反例
1.var a \[\]int
这样的写法实际上没有完成完全的初始化,没有分配内存空间,或是初始值,只是将指针指向一个空值无效地址0x0,len=0,cap=0
此时append操作是没问题的,
但是如果slice【0】是会panic因为指针是有问题的,
2.slice:=make(\[\]int,5)
这是正确的初始化方式一种,底层真实分配了地址空间,\[\]int 代表创建int类型的切片,这里同时给len和cap赋值为5,每个数据值初始化为0。当len和cap需要设置的值一样时可以这样写。
3.slice:=make(\[\]int,5,6)
当len和cap不一样时,需要这种写法,其中前一个参数代表len,后一个参数代表cap,在内存中,一共一个六个格子可以放数据,但是如例子初始化放了五个数据值为0,最后一个格子为空。
4.slice:=\[\]int{2,3,4}
直接表示法,len,cap在例子中同时为3
引用传递
首先这个引用传递其实是有点模棱两可的,只需要清晰理解下面这个例子就可以

如图在func test这个函数中我们创建了s,我们然后在另一个函数中将s作为参数传进来。注意这里在changeslice中相当于是再次创建了一个新切片s1 ,它是一个副本,只是它其中的字段和原s完全一样同样是,相同指针,长度,容量。
为什么称为引用传递 而不是值拷贝呢,我的理解是,这个unsafepointer指针指向的是同一个内存的地址 ,指向的底层真实内存地址是一样的,所有如果在这个函数中进行赋值操作,s0=-1,是会修改底层的内存所存储的数据的!

如下图,a与b之间是同样的值,很像值拷贝,但是由于他们指向的底层地址是一样的,所以看成引用传递。
但是注意这里是因为指针的性质才造成神似引用传递。实际上,这两个切片就是独立的,len和cap就是纯值拷贝了,在后面的函数中如果扩容,上面的函数中的切片长度或容量是不会改变的。后面会具体讲解
截取操作
Go
s:=[]int{1,2,3,4,5}
s0:=s[参数a:参数b]
s1:=s[1:]
s2:=s[:len(s)-1]
s3:=s[2:3]
截取操作也看成引用传递 ,这里其实就是截取了底层的slice中的一部分,但是会新创建一个slice header实例 slice header就是一个切片结构的克隆体,但是是浅拷贝,只拷贝指针,len和cap三个数据,底层数据是不拷贝的,值和克隆对象保持一样,指向同一片底层内存。
这里如代码s0表示的是创建一个新的切片实例,里面的数据是s中下标[a,b)的数,这里我用数学来解释,取值是左闭右开
如s3中,对应的只是原切片s中值为"3"这个元素,也就是下标为2的这个数,右边是不取的
其中不乏有两种经常用到的写法,如s1指的是,原s切片从下标为1的数开始一直往右,实际上的值就有2,3,4,5。
s2代表,从最左边一直到,len(s)-1的数,遵循左闭右开,实际上的值就有1,2,3,4
append操作

如图append操作,就是给原切片加数值。
append是可以同时加多个值的,如s=append(s,5,9,8),这里一次性加了三个值
这里就分两个情况,
第一是当append后新len<cap
这时就是正常给那些原来没有赋值的空格子内存,加上值就可以了,引发的只有len的改变
第二是当append后新len>cap
这里是很容易混淆的点,比如下面这个例子

图中的结果是正确的,但是很多人会以为最终结果是s=0,1,2,3,4,这种理解是错误的,因为append内部会判断当增加值后需不需要扩容操作,也就是len会不会大于cap,如果大于就会扩容,这个扩容的一个方法机制后面详细讲
实际的append结果就是s=0,0,0,0,0,0,1,2,3,4,
如果你需要改成s=0,1,2,3,4,这种形式,通过修改底层数据也就是下标访问才是对的,在for循环中执行si=i,直接讲原始的默认0值覆盖成新值
扩容机制
前面提到当append时,新len>cap时,会触发append内部的扩容,增大cap。
那么这样的一个扩容机制是这样的
文字叙述:
1.当实际需要的容量大于原本旧容量的两倍时直接使用实际需要的容量
如原本s={1,2},s=append(s,5,4,6,7,8),假设原来的len=2,cap=2,
当我append操作后, 新len=7,实际需要的cap=7,超过了旧cap的两倍,
那么这里新容量直接为7
2.当 实际需要容量小于 旧容量的两倍,且原容量<256时,
新容量设置为老容量的两倍
3.当实际需要容量小于 旧容量的两倍,且原容量>256时
新容量=旧容量+(旧容量+3/4*256)/4,
循环计算,直到新容量大于实际需要的容量为止。
append中还有一个很容易混淆的地方涉及扩容 如下
Go
s := make([]int, 10, 12)
s1 := s[8:9] // s1 = [0], len=1, cap=4
// 危险操作!
s1 = append(s1, 100)
// s1 变成 [0, 100]
// 但原 s[9] 也被改成了 100!(因为共享底层数组)
fmt.Println(s) // [0 0 0 0 0 0 0 0 0 100]
// 注意:s[9] 变成了100,s 被意外修改了!
不只是运用下标赋值,s9=100,才会改变底层数组!
当append过后,容量是够的的情况下也会改变底层数据!! 如上述代码,当我们给s1切片append过后,因为原本的容量是够的,那么虽然我们只是给s1这个截取切片append,也会使得原数据s中下标为9的位置变成100。也更好的说明了这就是引用传递
当append过后,容量是不够的的情况下不会改变底层数据,这时append会拷贝原数组值,再加上新值另找一个地址位置进行储存,形成两个独立的底层数组。
内存等级制度

在这个例子中我们通过扩容计算机制来说,这里是符合当实际需要容量小于旧容量的两倍,且原容量>256时这样一个条件的,通过计算不难得出答案为832,为什么和实际答案不符呢

这里设计一个空间等级制度(只能取固定的值),我们看到其实这里832个int对应的内存需要6656bytes,分配内存的mallocgc流程汇总,他是在等级48到49中间的,它只能取等级49,也就是分配的内存就是需要和对应等级给的内存为6784,再除以8得848。
删除操作

len变,cap不变
拷贝操作
前面我们都提到,其实不管创建多少个slice header实例都会指向同一片内存空间,底层的数据只有一份。如果说我 们需要完整拷贝,连底层数据也拷贝一份,在另一个地址出现,形成两份相同值但是不同地址的数据,需要怎么做呢?如下

copy函数,第一个参数是克隆体,第二个参数是被克隆体,
能实现深层拷贝。
一些重要问题
截取后的长度和容量
问题1:len=?,cap=? 
这里s1的len为2,cap为4 ,cap的数值是截取切片地址在原数据的地址开始一直往后到最大容量 ,和len没有关系!
截取切片是引用传递体现
问题:原数组会改变吗 
可以看到通过下标改s1,会使底层对应位置数据改变
截取切片到另一个方法使用后的len与cap变化
问题:s与s1的len与cap会改变吗 
答案是都不会变,因为对于在chengesllice这个方法中slice header是一个独立的实例,这这个方法中其实它已经在真实底层内存中加了值,但是原s与s1的len和cap就是不会变,只会在局部的方法changeslice中变化
值得注意的是因为s的长度还是10,就算物理意义上他的下标为10的地方有存在值的,但因为len没变,你访问是s10还是会panic