Go 切片
切片 (slice)是一个拥有相同类型元素的可变长序列。切片是围绕 动态数组 的概念构建的,它是基于数组类型做的一层封装,可以按需自动增长和缩小。切片一般用于快速地操作一块数据集合。
一、切片定义和赋值
1、定义
声明切片类型的基本语法如下:
var name []Type
其中 Type 是指切片的元素类型,name 指的是变量名
var a []string //声明一个字符串切片
var b []int //声明一个整型切片
注意:切片是引用类型,零值是 nil,一般需要初始化才能使用
2、字面量形式创建
可以用 组合字面量 来表示。对于一个元素类型为 T 的切片,它的值可以用形式 T{…}来表示(零值除外),这种方法和创建数组类似,初始的长度和容量会基于初始化时提供的元素的个数确定。中括号内包含 切片元素 (可以为空)及其 索引下标 ,切片元素之间用逗号隔开,索引下标和切片元素用冒号隔开,索引下标满足:
- 不能是变量,不可重叠,顺序可以打乱
- 如果是字面量,则它必须可以表示为非负的 int 值
- 如果是类型确定的常量,则必须为基本整数类型
- 如果是类型不确定的常量,但它必须是一个可以表示为 int 值的非负常量
可以省略,省略的索引下标为出现在它之前的元素的索引下标加一,如果出现的第一个元素的索引下标省略,则它的索引下标被认为是 0
同样的,如果是零值的元素,可以省略它(最后一个元素的不可省略)// 整型切片,其长度和容量都是3个元素 []int{0:0, 1:10, 2:20} // 完整形式 []int{0, 10, 20} // 省略索引下标 []int{2:20, 1:10, 0:0} // 打乱顺序 []int{1:10, 2:20} // 省略零值 []int{1:10, 20} // 省略零值和一部分索引下标
3、通过内置 make()函数创建
如果需要动态地创建一个切片,可以使用 make()内建函数,格式如下:
make([]Type, size, cap)
其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap(可省略)为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。
a := make([]int, 2)
b := make([]int, 2, 10)
4、数组或者切片截取
可以通过对数组或者切片截取一部分元素创建切片,格式如下:
slice[i:j]
slice[i:j:k]
其中:i(开始位置)表示从 slice 的第几个元素开始切,j(结束位置)控制切片的长度 (j-i),k 控制切片的容量(k-i)
从数组或切片生成新的切片拥有如下特性:
- 取出的元素数量为:j-i;
- 取出元素不包含结束位置对应的索引;
- 当缺省开始位置时,表示从连续区域开头到结束位置;
- 当缺省结束位置时,表示从开始位置直到末尾;
- 两者同时缺省时,与切片本身等效;
- 两者同时为 0 时,等效于空切片,一般用于切片复位。
- 需要指定 j 才能指定 k,且 k >=j
i、j、k 不大于目标数组或切片的长度
// 创建一个整型切片 // 其长度和容量都是 5 个元素 myNum := []int{10, 20, 30, 40, 50} // 创建一个新切片 // 其长度为 2 个元素,容量为 4 个元素 newNum := slice[1:3]
二、切片相关操作
1、切片复制
Go 语言的内置函数 copy 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
copy(destSlice, srcSlice []T) int
其中 srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),copy() 函数的返回值表示实际发生复制的元素个数。
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
2、添加元素
Go 语言的内建函数 append 可以为切片动态添加元素。 每个切片会指向一个底层数组,这个数组能容纳一定数量的元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的 底层数组就会更换。切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充。
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
注意:append 是一个变长参数函数,第一个参数为切片,且值可以为 nil
3、删除元素
Go 语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。
从开头位置删除:
删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
还可以用 copy 函数来删除开头的元素:
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
从中间位置删除:
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
从尾部删除:
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
4、切片遍历
切片是一个集合,可以迭代其中的元素。Golang 有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的元素, 遍历一个 nil 切片是允许的,这样的遍历可以看作是一个空操作。
for key, element = range slice {
// 使用key和element …
}
当迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。被遍历的切片值是一个副本,此副本是一个匿名的值,所以它是不可被修改的。range 创建了每个元素的副本,而不是直接返回对该元素的引用。
for-range 循环代码块有一些变种形式:
// 忽略键值循环变量。
for , element = range aContainer {
// …
}
// 忽略元素循环变量。
for key, = range aContainer {
element = aContainer[key]
// …
}
// 舍弃元素循环变量。此形式和上一个变种等价。
for key = range aContainer {
element = aContainer[key]
// …
}
// 键值和元素循环变量均被忽略。
for , = range aContainer {
// 这个变种形势没有太大实用价值。
}
// 键值和元素循环变量均被舍弃。此形式和上一个变种等价。
for range aContainer {
// 这个变种形势没有太大实用价值。
}
5、切片元素查看与赋值
一个切片 v 中存储的对应索引下标 k 的元素用语法形式 v[k]来表示,称 v[k]为一个元素索引表达式,通过对 v[k]赋值就可以改变对应索引下标 k 的切片元素值。
// 创建一个整型切片
// 其容量和长度都是 5 个元素
myNum := []int{10, 20, 30, 40, 50}
// 将索引为 1 的元素的值赋值给 a
a := myNum [1]
// 改变索引为 1 的元素的值
myNum [1] = 25
6、获取长度和容量
我们可以调用内置函数 len 来获取一个切片的长度,或者调用内置函数 cap 来获取一个切片的容量。这两个函数都返回一个 int 类型值,nil 切片(只声明未初始化)和空切片(初始化切片元素为空)的长度和容量都是 0,它们虽然在使用上没有差异,但是并不等价。
var s []int
fmt.Println(len(s), cap(s)) // 0 0
s, s2 := []int{2, 3, 5}, []bool{}
fmt.Println(len(s), cap(s), len(s2), cap(s2)) // 3 3 0 0
三、切片的内部结构
官方标准编译器对切片类型的内部定义大致如下:
type slice struct {
array unsafe.Pointer // 用来存储实际数据的数组指针,指向一块连续的内存
len int // 切片中元素的数量
cap int // array数组的长度
}
切片是一个数组片段的描述,它包含了指向数组的指针、片段的长度、和容量(片段的最大长度)。一个切片可以看作上述的结构体,空切片 array 字段指向 0,nil 切片 array 字段指向 nil(源代码中定义的 zerobase 值为 824634199592)地址。
需要注意的是:两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到