1. 定义
在Go语言中切片是一种数据结构,很便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得在访问速度以及垃圾回收优化等方面的好处。切片在Go语言的源码定义如下所示,由于其数据结构中有指向底层数组的指针,所以切片是一种引用类型。
// src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
2. 内部实现
切片是一个很小的对象,对底层数组进行了抽象。切片的数据结构有3个字段,分别是指向底层数组的指针array,切片中元素的个数len(即长度)和切片的最大容量cap。如下图所示。
3. 切片的创建
(1) 由数组创建
创建语法为array[b:e], 其中array表示数组名;b表示索引开始位,可以不指定,默认为0;e表示索引结束位,可以不指定,默认是len(array),[b:e]区间是“左闭右开”,即第一个元素是array[b],最后一个元素是array[e-1]。例如:
// 创建有7个int类型元素的数组
var array = [...] int{0,1,2,3,4,5,6}
s1 := array[0:4]
s2 := array[:4]
s3 := array[2:]
fmt.Println(s1) // [0 1 2 3]
fmt.Println(s2) // [0 1 2 3]
fmt.Println(s3) // [2 3 4 5 6]
(2) 通过内置函数make创建切片
切片使用之前需要make是因为make操作要完成切片底层数组的创建及初始化,由make创建的切片个元素被默认初始化位切片元素类型的默认值。例如:
// len = 10, cap = 10
a := make([]int, 10)
// len = 10, cqp = 15
b := make([]int, 10, 15)
fmt.Println(a) // 结果为 [0 0 0 0 0 0 0 0 0 0]
fmt.Println(b) // 结果为 [0 0 0 0 0 0 0 0 0 0]
// 直接声明切片类型变量是没有意义的(使用前必须make)
var c []int
fmt.Println(c) // 结果为 []
(3) 通过切片字面量来创建切片
// 创建长度和容量都是5个元素的字符串切片
s1 := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 注意和字符串数组区别([]和[...]), 下面是声明数组
arr := [...]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 创建长度和容量都是3个元素的整形切片
s2 := []int{10, 20, 30}
(4) 使用索引创建切片(比较少用)
// 创建字符串切片
// 使用空字符初始化第100个元素
s3 := [] string{99: ""}
(5) 创建空切片
// 使用make创建空的整形切片
s1 := make([]int, 0)
// 使用切片字面量创建空的整形切片
s2 := []int{}
空切片底层数组包含0个元素,也没有分配任何存储空间。不管使用nil切片(没有make的切片)还是空切片,对其调用内置函数的效果都是一样的。
4. 切片支持的操作
- 内置函数len()返回切片长度
- 内置函数cap()返回切片底层数组容量
- 内置函数append()对切片追加元素
- 内置函数copy()用于复制一个切片
示例如下:
a := [...]int{0,1,2,3,4,5,6}
b := make([]int, 2, 4)
c := a[0:3]
fmt.Println(len(b)) // 结果为 2
fmt.Println(cap(b)) // 结果为 4
// 切片b中添加元素
b = append(b,1)
fmt.Println(b) // 结果为 [0 0 1]
fmt.Println(len(b)) // 结果为 3
// 切片b中添加切片
b = append(b, c...)
fmt.Println(b) // 结果为 [0 0 1 0 1 2]
fmt.Println(len(b)) // 结果为 6
fmt.Println(cap(b)) // 结果为 8 底层数组发生拓展
d := make([]int, 2, 2)
copy(d, c) // copy只会复制c和d中长度最小的
fmt.Println(d) // 结果为 [0 1]
fmt.Println(len(d)) // 结果为 2
fmt.Println(cap(d)) // 结果为 2
5. 切片的使用
(1) 赋值
对切片中的某个元素赋值和数组中元素赋值的方法完全一样。用[]操作符就可以改变某个元素的值,代码如下:
// 创建一个整形切片
// 其容量和长度都为5个元素
s := []int{10, 20, 30, 40, 50}
// 改变索引为1的元素的值
s[1] = 200
(2) 对切片进行切片
切片之所以称为切片,是因为创建一个切片就是把底层数组切出一部分,代码如下:
// 创建一个整形切片
// 其容量和长度都为5个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2个元素,容量为4个元素
newSlice := slice[1:3]
fmt.Println(newSlice) // 结果为 [20 30]
对切片进行切片后,其内存中的分布如下图所示:
计算切片的长度和容量:
对底层数组容量是k的切片slice[i:j]来说
切片长度:j - i, 切片容量:k - i
例如,对于图2中的切片newSlice, 长度为3 - 1 = 2, 容量为5 - 1 = 4。
(3) 修改切片内容
// 创建一个整形切片
// 其容量和长度都为5个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2个元素,容量为4个元素
newSlice := slice[1:3]
// 修改newSlice索引为1的元素,同时也修改了原来的slice索引为2的元素
newSlice[1] = 32
fmt.Println(newSlice) // 结果为 [20 32]
fmt.Println(slice) // [10 20 32 40 50]
切片只能访问到其长度内的元素。试图访问超出其长度的元素将会导致语言运行时异常。
(4) 切片增长
相对于数组而言,使用切片的一个好处是可以通过append按需增加切片的容量。
要使用append,需要一个被操作的切片和一个要追加的值,当append调用返回时,会返回一个包含修改结果的新切片。
// 创建一个整形切片
// 其容量和长度都为5个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2个元素,容量为4个元素
newSlice := slice[1:3]
// append前
fmt.Println(newSlice) // 结果为 [20 30]
fmt.Println(slice) // [10 20 30 40 50]
// append后
newSlice = append(newSlice, 300, 400)
fmt.Println(newSlice) // 结果为 [20 30 300 400] (发生变化)
fmt.Println(slice) // [10 20 30 300 400] (原来的位置上的元素被覆盖)
我们发现,当newSlice执行append后,slice也发生了变化,append操作前后,底层数组如下图所示。
由于newSlice在底层数组里还有额外的容量可用,所以在append的时不会创建新的底层数组,而是继续在原来的底层数组上操作,但由于newSlice和原始的slice共享一个底层数组,所以slice中索引为3和4的元素的值也被改动了
我们再来看另外一种特殊的情况,代码如下。
// 创建一个整形切片
// 其容量和长度都为5个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为4个元素!!!容量为4个元素
newSlice := slice[1:5]
// append前
fmt.Println(newSlice) // 结果为 [20 30 40 50]
fmt.Println(slice) // [10 20 30 40 50]
// append后
newSlice = append(newSlice, 300, 400)
fmt.Println(newSlice) // 结果为 [20 30 40 50 300 400] (发生变化)
fmt.Println(cap(newSlice)) // 结果为8,表明newSlice底层数组的容量拓展为原来的2倍
fmt.Println(slice) // [10 20 30 40 50] (没有发生变化!!!)
和上段代码不一样的是,我们在这段代码修改了newSlice的长度,使newSlice的容量被全部占满,当再次进行append操作得时候,newSlice的元素发生了变化,而slice的元素没有没有发生,主要原因是,当append操作时发现当前底层的容量不够用时,会创建一个新的底层数组,将现有数组的值复制到新数组里,此时newSlice和slice不在共享同一个底层数组,因此对newSlice执行append后,slice的元素不会发生变化。, 底层数组的变化如下图所示。
而且,append函数会智能地处理底层数组的容量增长。在切片容量小于1000个元素时,总是成倍地增加容量。一旦元素超过1000,扩容因子会设为1.25,也就是每次增加25%的容量。
(5) 切片使用的一个重要细节
如果创建切片时设置切片容量和长度一样,就可以强制让新切片的第一个append操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续操作。
(6) 在函数间传递切片
在64位架构的机器上,一个切片需要24字节的内存(3个字段)。由于切片关联的数据都包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时时候,只复制切片本身,不会涉及底层数组。
在函数间传递24字节的数据会非常简单快速。这也是切片效率高的地方。不需要传递指针和复杂的语法,只需要复制切片,按想要的方式修改数据,然后返回一份新的切片副本。示例代码如下。
package main
// 函数foo接收一个整形切片,并返回这个切片
func foo(slice []int) [] int {
// ...
return slice
}
func main() {
// 分配100万个整形值的切片
slice := make([]int, 1e6)
// 将slice传递到函数foo
slice = foo(slice)
}
切片用法的演示就到这里了~~~
我是lioney,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。
个人笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎大家指出,也欢迎大家一起交流后端各种问题!
有疑问加站长微信联系(非本文作者)