前言
本篇博客不会贴出go的源码,只会告诉你slice用法,因为我们学习一项技术主要学的是编程的思想,眼花缭乱的源码千篇一律,深入人心的思想万里挑一,博客种通过图文的方式介绍底层机制,为的是能让开发人员写出属于自己的技术,而不是生搬硬套去复制别人的代码,相信等你理解了底层原理之后,你完全可以自己写一个动态数组出来,这就是我写博客的初心!
slice是什么
在go语言中,如果想要使用一个连续的内存空间,你可以使用数组,但是数组是固定长度的,无法做到动态扩容。因此切片slice就出现了,你可以先给切片设置一个初始容量,然后往里面添加元素,当切片容量不足时会自动扩容,来装载加入的元素;
用法
// 初始化长度为5,容量为10的切片
strings := make([]string, 5, 10)
// 创建string类型的切片,长度为5,容量为5
strings1 := make([]string, 5)
// 添加元素
strings = append(strings , "元素")
// 修改元素
strings[1] = "修改的元素"
删除元素
切片本身并没有提供删除的方法,但是我们可以自己实现一个
func main(){
strings := make([]string, 0, 2)
strings = append(strings, "one")
strings = append(strings, "two")
strings = append(strings, "three")
strings = append(strings, "four")
newStrings := deleteSlice(strings, "three")
fmt.Println(newStrings)
}
/**
* 删除指定切片的元素
* strings 原切片
* 需要删除的元素
*/
func deleteSlice(strings []string, delStr string) []string{
newStrings := make([]string, 0, len(strings))
// 删除元素 three--排除需要删除的元素,将其他元素赋值给新的切片
for _, v := range strings {
if v != delStr {
newStrings = append(newStrings, v)
}
}
fmt.Printf("删除后的元素%v \n", newStrings)
fmt.Println(len(newStrings))
return newStrings
}
切片底层用什么结构?
在开始的时候就有介绍过,切片属于动态数组,所以他的底层就是一个数组,在内存中是一段连续的内存空间
长度 和 容量的区别
切片的长度指的是已初始化元素长度大小,什么意思呢?比如下面这个语句
strings := make([]string, 5, 10)
这个语句表示切片的长度未5,容量为10,
- 切片的容量指的就是底层数组的长度,容量为10,就表示底层数组的长度就是10
- 切片的长度指的是已初始化的元素个数,从下图可以看到,前5个元素已经分配了内存空间,并赋予初始值
""
(空字符串),所以它的长度是5
可通过以下方式得出切片长度和容量
strings := make([]string, 5, 10)
fmt.Printf("长度:%d, 容量:%d",len(strings),cap(strings))
打印结果如下:
长度:5, 容量:10
扩容机制
以上了解了那么多,那么切片的在内部是如何进行扩容的呢?接下来就来揭晓它内部的秘密,首先我们声明一个切片,长度为1
,容量为2
,然后往这个容量中添加4
次元素,看看有什么样的变化
strings := make([]string, 1, 2)
for i := 0; i < 4; i++ {
strings = append(strings,"1")
fmt.Printf("第%d次添加元素,长度:%d,容量:%d \n",i + 1,len(strings),cap(strings))
}
fmt.Println(strings)
控制台打印结果如下
第1次添加元素,长度:2,容量:2
第2次添加元素,长度:3,容量:4
第3次添加元素,长度:4,容量:4
第4次添加元素,长度:5,容量:8
[ 0 1 2 3 ]
扩容分析
通过以上案例,我们可以分解出每次操作后底层的数组都做了哪些事
0、创建切片
首先,当我们创建好切片后,长度为1,容量为2,长度为1就表示第0个元素已经初始化好了,已经赋值为空字符串了,此时结构如下图
1、第一次添加元素"0"
因为第0个下标的的位置已经被占用了,所以添加的元素就会往后面排,因此会先给下标1的元素先初始化,然后将"0"
的值为放到1的位置上,此时大家会发现,容量已经满了,但是这时候还没触发扩容;
2、第二次添加元素"1"
在添加元素前,会先判断数组的容量是否足够,因为这时候容量已经满了,所以一定会触发扩容,扩容原理如下
- 创建一个新的数组,容量为原数组的2倍
- 将原数组的内容复制到新数组
- 将新添加的元素加入到新数组的后面
- 原数组因为没在使用,稍后gc会将其回收
扩容后的结构如下图 (==白色部分表示未分配内存空间==)
3、第三次添加元素"2"
这一步是正常的追加元素,追加到后面,和第一次添加元素时一样,容量又满了,但是还未触发扩容
4、第四次添加元素"3"
同样地,在添加元素前,会先判断数组的容量是否足够,因为这时候容量已经满了,所以会再次触发扩容
每次扩容都扩一倍吗?
不是的,,如果每次都扩一倍的话,将会占用大量的内存空间,而很有可能这些空间我们都用不到;所以go语言为了防止数组冗余,做了一些处理
- 当数组容量小于
1024
时,每次扩容一倍 - 当数组容量大于等于
1024
时,每次扩容0.25
倍,也就是扩容四分之一的容量
多说无益,我们来测试下
strings := make([]string, 1, 2)
for i := 0; i < 1024; i++ {
strings = append(strings,fmt.Sprint(i))
}
fmt.Printf("经过1024次添加元素,长度:%d,容量:%d \n",len(strings),cap(strings))
打印结果如下,按照计算公式: 1024 * 1.25 = 1280
,扩容后就是1280的容量
经过1024次添加元素,长度:1025,容量:1280
切片会缩容吗?
切片不会缩容,扩容后的数组空间,哪怕你不用,也是安安静静地占用着内存空间的,不会进行缩容;