对切片的理解
GO 中的数组是固定长度的数据结构,而切片可以理解为一个动态的数组的概念。
它基于数组并提供了动态扩容的 API,在使用上可以理解为 Java 中的 ArrayList,但是其底层还是有非常大的区别的。
切片的组成
切片主要包含三个部分
指向底层的数组的指针(pointer)
容量(capacity)
长度(length)
从组成可以看到,切片本身是不包含数组而是拥有一个指向底层数组的指针,这个和 Java 中的 ArrayList 不同.
因为每个 ArrayList 都拥指向自己独有的数组的"指针",而对于 GO 的切片来说可能存在多个切片对应同一个底层数组的情况。
切片基础原理
定义一个切片的代码如下:
slice := make([]string, 2)
fmt.Println("容量:", cap(slice), "长度:", len(slice))
// 输出:容量: 2 长度: 2
slice = make([]string, 2, 3)
fmt.Println("容量:", cap(slice), "长度:", len(slice))
// 输出:容量: 3 长度: 2
复制代码
复制代码
可以看到,如果不指定容量,那么容量默认和长度相同。 那么执行完 make 之后,内部的数据结构是如何呢?以slice = make([]string, 2, 3)
为例:
如图,底层会创建一个以切片容量为长度的数组,并将切片的指针指向数组的第一个元素,此时对切变的访问会根据切边的长度(length)来做限制。
此时切片的 length 是 2,如果此时去访问 slice 的第 3 个元素,就会产生错误:
println(slice[2])
// 运行报错:panic: runtime error: index out of range [2] with length 2
复制代码
复制代码
切片的英文单词是slice
,这个名字是有意义的,我们可以从一个切片中“切”出一个新的切切片。 这个操作如下所示:
newSlice := slice[1:2:3]
fmt.Println("容量:", cap(newSlice), "长度:", len(newSlice))
// 输出:容量: 2 长度: 1
复制代码
复制代码
这时候底层的结构如下:
根据上面的图我们理解一下slice[1:2:3]
后面三个数字的意义:
第一个下标,这里是 1:指的是新切片从原先的切片指向的数组索引为 1 的位置开始,这里就是指定新切片的起始下标;
第二个下标,这里是 2:指的是新切片的长度在原先数组的位置,这里指定为 2(不是数组索引),起始位置为 1,所以新切片的长度就是 2-1=1;
第三个下标:这里是 3:指的是新切片的容量在原先数组的位置,这里指定为 3(不是数组索引),起始位置为 1,所以新切片的容量就是 3-1=2;
这里注意,如果指定的三个下标的数值超过了原先底层的数组的长度(不是索引),会报数组越界错误。
此时这里两个切片共享一个底层数组,对其中任何一个切片的元素进行修改,都会产生相互影响。
slice := make([]string, 2)
slice = make([]string, 2, 3)
newSlice := slice[1:2:3]
newSlice[0] = "张三"
printSlice(slice, "slice")
printSlice(newSlice, "newSlice")
func printSlice(slice []string, name string) {
fmt.Println("-----开始打印" + name + "的切片元素")
for index, item := range slice {
fmt.Println(name+" 索引为", index, "位置的数据为:"+item)
}
fmt.Println(name, "长度为:", len(slice), " 容量为:", cap(slice))
fmt.Println("-----结束打印" + name + "的切片元素")
}
// -----开始打印slice的切片元素
// slice 索引为 0 位置的数据为:
// slice 索引为 1 位置的数据为:张三
// -----结束打印slice的切片元素
// -----开始打印newSlice的切片元素
// newSlice 索引为 0 位置的数据为:张三
// -----结束打印newSlice的切片元素
复制代码
复制代码
切片与 append 函数
当我们定义好切片之后,需要添加元素我们需要使用append
方法。
slice := []string{"茄子", "土豆", "黄瓜", "西瓜"}
newSlice := slice[1:3:4]
printSlice(slice, "slice")
printSlice(newSlice, "newSlice")
//-----开始打印slice的切片元素
//slice 索引为 0 位置的数据为:茄子
//slice 索引为 1 位置的数据为:土豆
//slice 索引为 2 位置的数据为:黄瓜
//slice 索引为 3 位置的数据为:西瓜
//slice 长度为: 4 容量为: 4
//-----结束打印slice的切片元素
//-----开始打印newSlice的切片元素
//newSlice 索引为 0 位置的数据为:土豆
//newSlice 索引为 1 位置的数据为:黄瓜
//newSlice 长度为: 2 容量为: 3
//-----结束打印newSlice的切片元素
复制代码
复制代码
此时底层数组的结构如图所示,其中,slice 的长度和容量都是 4,即整个底层数组,而 newSlice 指定了slice[1:3:4]
,即其本身的容量是 3,但是此时切片长度为 2
此时我们执行第一次添加数据:
newSlice = append(newSlice, "香蕉")
printSlice(slice, "第一次添加数据后:slice")
printSlice(newSlice, "第一次添加数据后:newSlice")
//-----开始打印第一次添加数据后:slice的切片元素
//第一次添加数据后:slice 索引为 0 位置的数据为:茄子
//第一次添加数据后:slice 索引为 1 位置的数据为:土豆
//第一次添加数据后:slice 索引为 2 位置的数据为:黄瓜
//第一次添加数据后:slice 索引为 3 位置的数据为:香蕉
//第一次添加数据后:slice 长度为: 4 容量为: 4
//-----结束打印第一次添加数据后:slice的切片元素
//-----开始打印第一次添加数据后:newSlice的切片元素
//第一次添加数据后:newSlice 索引为 0 位置的数据为:土豆
//第一次添加数据后:newSlice 索引为 1 位置的数据为:黄瓜
//第一次添加数据后:newSlice 索引为 2 位置的数据为:香蕉
//第一次添加数据后:newSlice 长度为: 3 容量为: 3
//-----结束打印第一次添加数据后:newSlice的切片元素
复制代码
复制代码
这里可以看到,对 newSlice 进行 append 添加数据之后,也会同时修改 slice 的数据,这是因为其底层的数据是共享的。append 中后的结构如下
这里,对 newSlice 进行添加数据的时候,因为本身容量为 3,当前长度是 2,它还有空间,所以会直接将数据'香蕉'覆盖原先的'西瓜',因为对于 newSlice 来说,本身'西瓜'位置的元素对他来说是未使用的。
此时我们再对 newSlice 进行第二次添加数据
newSlice = append(newSlice, "苹果")
printSlice(slice, "第二次添加数据后:slice")
printSlice(newSlice, "第二次添加数据后:newSlice")
//-----开始打印第二次添加数据后:slice的切片元素
//第二次添加数据后:slice 索引为 0 位置的数据为:茄子
//第二次添加数据后:slice 索引为 1 位置的数据为:土豆
//第二次添加数据后:slice 索引为 2 位置的数据为:黄瓜
//第二次添加数据后:slice 索引为 3 位置的数据为:香蕉
//第二次添加数据后:slice 长度为: 4 容量为: 4
//-----结束打印第二次添加数据后:slice的切片元素
//-----开始打印第二次添加数据后:newSlice的切片元素
//第二次添加数据后:newSlice 索引为 0 位置的数据为:土豆
//第二次添加数据后:newSlice 索引为 1 位置的数据为:黄瓜
//第二次添加数据后:newSlice 索引为 2 位置的数据为:香蕉
//第二次添加数据后:newSlice 索引为 3 位置的数据为:苹果
//第二次添加数据后:newSlice 长度为: 4 容量为: 6
//-----结束打印第二次添加数据后:newSlice的切片元素
复制代码
复制代码
此时可以看到,newSlice 的容量直接扩容了两倍,而 slice 的切片的容量没有变化!这是怎么回事呢,看下图:
这里 newSlice 进行 append 的时候,因为其 length 和容量相等,即其本身的元素已经慢了,这时候添加元素底层数组也没有空间了,这时候会创建一个容量双倍的底层数组,并将原本的数据进行一次数据的拷贝,放入新创建的数组中,这时候再将数据'苹果'放入新添加的数组中,而新的切片会会将数组的指针,指向新创建的数组的第一个元素。
小结
对于切片,它是一种不可变的数据结构,对数组进行赋值都是操作的底层的数组
对切片进行append
操作,每次都会创建新的 slice 对象,所以每次append
之后,都要为局部变量重新赋值
扩容的容量,在容量小于 1000 时,每次都是双倍扩容,当超过 1000 之后每次都是 1.25 倍扩容
最后
如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163 相互学习,我们会有专业的技术答疑解惑
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点 star:http://github.crmeb.net/u/defu不胜感激 !
PHP 学习手册:https://doc.crmeb.com技术交流论坛:https://q.crmeb.com
评论