系列文章
【golang学习之旅】使用VScode安装配置Go开发环境
【golang学习之旅】报错:a declared but not used
【golang学习之旅】Go 的基本数据类型
【golang学习之旅】深入理解字符串string数据类型
【golang学习之旅】go mod tidy
【golang学习之旅】记录一次 panic case : reflect: reflect.Value.SetInt using unaddressable value
【golang学习之旅】记录一次 error case : full error output: cc1: error: unrecognized command line option
【golang学习之旅】Go程序快速开始 & Go程序开发的基本注意事项
【golang学习之旅】Go语言常用转义字符
【golang学习之旅】Go中的变量(1)
【golang学习之旅】Go中的变量——基本数据类型(1)
【golang学习之旅】Go中的变量——基本数据类型(2)
【golang学习之旅】复杂数据类型——指针 & 函数
【golang学习之旅】延迟调用——defer
【golang学习之旅】复杂数据类型——数组
1. 理解切片(slice)
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集
简单地说,切片就是一种简化版的动态数组。
因为动态数组的长度是不固定,切片的长度自然也就不能是类型的组成部分了。数组虽然有适用它们的地方,但是数组的类型和操作都不够灵活,因此在 Go 代码中数组使用的并不多。而切片则使用得相当广泛,理解切片的原理和用法是一个 Go 程序员的必备技能。
2. 切片的定义
2.1 切片的内存结构
我们先看看切片的结构定义,reflect.SliceHeader
:
切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。
type SliceHeader struct {
Data uintptr // 指向底层数组的指针
Len int // 切片的长度(即元素的数量)
Cap int // 切片的容量(即底层数组可以容纳的元素数量)
}
现在,考虑以下两个切片的声明和初始化:
x := []int{2, 3, 5, 7, 11}
y := x[1:3]
- x切片的内存结构
Data
:指向一个包含至少5个int类型元素的数组的指针。具体地,这个数组包含元素[2, 3, 5, 7, 11]。Len
:5,因为x包含5个元素。Cap
:5,因为底层数组也恰好有5个元素的空间,所以切片x的容量等于其长度。
- y切片的内存结构
Data
:同样指向那个包含[2, 3, 5, 7, 11]的数组,但切片y的视角是从第二个元素开始的,即3(索引为1
,因为索引从0开始)。Len
:2,因为y是通过x[1:3]创建的,所以它包含x中从索引1到索引2(不包含3)
的元素,即[3, 5]。
Cap
:4,尽管y只包含了2个元素,但它的容量是从y的起始元素到原数组x末尾的元素数量,即[3, 5, 7, 11]这四个元素的空间。这是因为y和x共享同一个底层数组,所以y的容量受到这个共享数组的限制。
- 示意图
下面是一个简化的示意图,用于说明这两个切片的内存结构:
2.2 创建切片
在Go中,我们可以通过多种方式创建切片:
- 直接使用数组创建切片:通过数组的一部分来初始化切片。
a := [5]int{1, 2, 3, 4, 5}
s := a[1:4] // s是一个切片,包含a中索引为1到3的元素(即2, 3, 4)
- 使用
make
函数:make函数是Go中用于分配和初始化内置类型的内置函数,也可以用来创建切片。
s := make([]int, 0, 5) // 创建一个长度为0、容量为5的int类型切片
- 切片字面量:直接使用切片字面量创建切片。
s := []int{1, 2, 3} // 直接初始化一个切片
还有一些比较特殊的切片:
- nil 切片。未初始化的切片默认值为nil
var a []int
- 空切片,也叫零值切片。和 nil 不相等, 一般用来表示一个空的集合。len 和 cap 都为 0。空切片在内部拥有一个非nil的、零长度的底层数组
var b = []int{}
- 在判断一个切片是否为空时,一般通过
len
获取切片的长度来判断,一般很少将切片和 nil 值做直接的比较
3. 操作切片
3.1 添加切片元素
3.1.1 append
使用append函数可以向切片末尾追加一个或多个元素。如果追加的元素超出了当前切片的容量,append会分配一个新的底层数组,并将原切片的内容以及新元素复制到新数组中。
var a []int
a = append(a, 1) // 追加 1 个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加 1 个切片, 切片需要解包
这里需要注意...
(三个点,称为“变参”或“展开运算符”)。在Go中,当你将切片或数组作为函数参数,并且希望将它的所有元素作为独立的参数传递给函数时,可以使用这个操作符。在这个上下文中,...
将 []int{1,2,3}
切片中的所有元素“展开”成独立的参数,传递给 append 函数。
除了在切片的尾部追加,我们还可以在切片的开头添加元素:
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加 1 个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加 1 个切片
在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制 1 次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。
3.1.2 copy
copy函数可以用来复制切片的内容:
src := []int{1, 2, 3}
dst := make([]int, 3)
copy(dst, src) // 现在dst是[1, 2, 3]的拷贝
可以用 copy 和 append 组合可以避免创建中间的临时切片,同样是完成添加元素的操作:
a = append(a, 0) // 切片扩展 1 个空间
copy(a[i+1:], a[i:]) // a[i:] 向后移动 1 个位置
a[i] = x // 设置新添加的元素
- 第一句 append 用于扩展切片的长度,为要插入的元素留出空间
- 第二句 copy 操作将要插入位置开始之后的元素向后挪动一个位置
- 第三句真实地将新添加的元素赋值到对应的位置
用 copy 和 append 组合也可以实现在中间位置插入多个元素(也就是插入一个切片):
用 copy 和 append 组合也可以实现在中间位置插入多个元素(也就是插入一个切片):
a = append(a, x...) // 为 x 切片扩展足够的空间
copy(a[i+len(x):], a[i:]) // a[i:] 向后移动 len(x) 个位置
copy(a[i:], x) // 复制新添加的切片
稍显不足的是,在第一句扩展切片容量的时候,扩展空间部分的元素复制是没有必要的。没有专门的内置函数用于扩展切片的容量,append 本质是用于追加元素而不是扩展容量,扩展切片容量只是 append 的一个副作用。
3.2 删除切片元素
根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。
其中删除切片尾部的元素最快:
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部 1 个元素
a = a[:len(a)-N] // 删除尾部 N 个元素
删除开头的元素可以直接移动数据指针:
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 个元素