Go 切片

admin
admin 2020年03月12日
  • 在其它设备中阅读本文章

切片 (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)地址。
需要注意的是:两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到