目录
0 个人理解
数组的核心概念是具有固定长度且相同类型的在内存中连续的数据序列,这句话其实不难理解,我来做一些说明:
- 固定长度:在大多数编程语言中,数组的长度是在定义时便固定好的,这主要是为了申请内存,不管是在静态区、栈还是堆,当然,数组长度在一开始便固定下来不见得是一件好事,你实际需要的长度未必与定义数组时声明的长度等价,如果你声明的数组长度太大,那势必会造成空间的浪费,否则,当需要添加的元素个数超出数组固定长度时,如何处理后面的元素也是大老难的问题。因此,C++有vector,java有ArrayList,Go语言有Slice(切片),都是为了解决数组长度是固定的这一问题的产物
- 相同类型:上面提到,数组固定长度是为了方便申请内存,相同类型这一特性也是为了此目的,当然,一个数组里面的数据类型都是相同的也更符合人类直觉,否则便是python中的元组之类的概念了。总而言之,当我们提出我们需要某种类型的长度为多少的数组的需求之后,编辑器就可以计算出来我们需要多少连续的内存
- 内存中连续:在与链表对比时,数组的优势便是随机访问元素的时间复杂度是O(1),这主要得益于其在内存中连续的特性,当我们需要找到数组的第n个元素,即arr[n-1],只需要将数组首地址向后偏移n * 元素类型大小即可定位到对应元素
总的来说,数组是一种不够灵活但在某些操作时又比较方便的数据类型,而切片是为了方便对数组进行操作的一种新型数据结构,接下来总结一下Go语言中这两者的用法
1 Go数组
1.1 定义和初始化
1.1.1 定义数组
- 定义数组需指明元素类型和数组长度
- 未初始化的数组,其元素会被初始化为对应元素类型的默认值
// 定义数组
var arr [5]int
fmt.Println(arr)
该数组的输出为[0 0 0 0 0]
1.1.2 初始化
数组的初始化有以下三种方式:
- 显式初始化
// 显式初始化数组
var arr1 = [5]int{1, 2, 3, 4, 5}
fmt.Println(arr1)
- 忽略长度,显式初始化
// 省略长度,自动计算长度
var arr2 = [...]int{1, 2, 3, 4, 5}
fmt.Println(arr2)
- 根据索引初始化部分元素值
// 指定索引初始化
var arr3 = [...]int{1: 1, 3: 3, 5: 5}
fmt.Println(arr3)
以上三段代码的执行结果为:
1.2 访问和修改元素
- 与C语言等编程语言一致,直接使用下标n - 1访问或修改数组的第n个元素,如
arr[4] = 6
即表示将数组arr的第5个元素的值修改为6
2 Go切片
切片可以视为为数组开辟的一个访问窗口,其一定是与一个数组绑定的,具有以下特点:
- 动态长度:切片的长度可以动态变化。
- 引用类型:切片是引用类型,多个切片可以引用同一底层数组,修改一个切片会影响其他引用相同数组的切片。
- 底层数组:切片实际上是对底层数组的引用,包含指向数组的指针、长度和容量。
2.1 定义与初始化
2.1.1 定义
- 定义方式与数组基本一致,只是取消了长度的限制,且必须初始化后才能访问或修改元素
// 定义切片
var sl []int
fmt.Println(sl)
// len 和 cap 为 0
fmt.Println(len(sl))
fmt.Println(cap(sl))
2.1.2 初始化
- 切片可以直接显式初始化,或者使用make函数、基于数组进行初始化:
- 显式初始化
// 显式初始化 sl1 := []int{1, 2, 3, 4, 5} fmt.Println(sl1) fmt.Println(len(sl1)) fmt.Println(cap(sl1))
- 使用make函数
// 使用make函数 // 指定长度 sl2 := make([]int, 5) fmt.Println(sl2) fmt.Println(len(sl2)) fmt.Println(cap(sl2)) // 指定长度和容量 sl3 := make([]int, 5, 10) fmt.Println(sl3) fmt.Println(len(sl3)) fmt.Println(cap(sl3))
- 基于数组
// 基于数组初始化切片 arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} sl4 := arr[2:6] fmt.Println(sl4) fmt.Println(len(sl4)) fmt.Println(cap(sl4))
以上代码运行结果如下:
2.2 访问与修改元素
与数组一致,使用下标访问或修改元素
2.3 其他操作
2.3.1 获取长度和容量
fmt.Println(len(s)) // 获取长度
fmt.Println(cap(s)) // 获取容量
2.3.2 添加元素
s := []int{1, 2, 3}
s = append(s, 4, 5)
2.3.3 分片
// 基于切片创建切片
full := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(full) // 输出原切片
fmt.Println(len(full)) // 输出原切片长度
fmt.Println(cap(full)) // 输出原切片容量
part1 := full[2:8]
fmt.Println(part1) // 输出 part1 切片
fmt.Println(len(part1)) // 输出 part1 长度
// 切片的容量等于数组容量减去切片初始位置
fmt.Println(cap(part1)) // 输出 part1 容量(cap = 10 - 2)
part2 := full[2:]
// 切片进行扩容后,可能会影响原切片,取决于切片的容量
part2 = append(part2, 11) // 可能分配新的底层数组
part2[0] = 0 // 修改 shared 底层数组
fmt.Println(full) // 可能被 part2 修改
fmt.Println(len(full))
fmt.Println(cap(full))
fmt.Println(part2)
fmt.Println(len(part2))
fmt.Println(cap(part2))
// 原切片进行扩容,也可能影响基于原切片衍生的切片
part3 := full[:]
full = append(full, 11) // 可能分配新的底层数组
fmt.Println(full) // 新数组
fmt.Println(len(full))
fmt.Println(cap(full))
fmt.Println(part3) // 旧数组
fmt.Println(len(part3))
fmt.Println(cap(part3))
运行结果:
2.3.4 拷贝
- 使用copy()函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值,也就是说,拷贝过程中不会发生扩容
sl1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl2 := make([]int, 5)
copy(sl2, sl1)
fmt.Println(sl2)
fmt.Printf("len of sl2 : %d\n", len(sl2))
fmt.Printf("cap of sl2 : %d\n", cap(sl2))
运行结果:
3 底层实现
3.1 底层结构
在Go语言中,切片的底层结构是一个包含三个字段的数据结构:
- 指向底层数组的指针(pointer):指向实际存储数据的数组。
- 切片的长度(length):表示切片中元素的数量。
- 切片的容量(capacity):表示从切片的起始位置到底层数组末尾的元素数量。
type SliceHeader struct {
Data uintptr // 指向底层数组的指针
Len int // 切片的长度
Cap int // 切片的容量
}
3.2 扩容机制
3.2.1 扩容的时机
当切片的元素个数超过底层数组的容量时,就需要进行扩容。切片的容量大小是指其底层数组的大小,而长度是切片中实际存储的元素个数
3.2.2 扩容规则
当切片需要扩容时,Go 语言会按照一定的策略重新分配底层数组,并将原有数据复制到新的数组中。具体扩容的规则如下:
- 如果切片的长度(即实际元素数量)小于 1024,那么在扩容时新数组的大小将扩大为原数组的 2 倍。即新容量将是原容量的 2 倍。
- 如果切片的长度大于等于 1024,则在扩容时新数组的大小只会增加原数组容量的 25%。也就是说,新容量将是原容量的 1.25 倍。
- 如果扩容后的新容量仍然不足以存储新的元素,那么会继续按照上述规则进行扩容,直到新容量足够大
3.2.3 扩容的影响
切片的扩容会导致底层数组的重新分配和数据的拷贝,因此扩容的成本是相对较高的。在大量操作切片的场景中,尽量提前估计所需容量,以减少不必要的扩容次数,从而提高性能
3.2.4 示例
// 创建初始长度为 5,容量为 5 的切片
s := make([]int, 5)
fmt.Println("Length:", len(s), "Capacity:", cap(s)) // 输出 Length: 5 Capacity: 5
// 追加元素,此时长度为 6,超过了初始容量,触发扩容
s = append(s, 6)
fmt.Println("Length:", len(s), "Capacity:", cap(s)) // 输出 Length: 6 Capacity: 10
// 追加元素,继续触发扩容
for i := 7; i <= 15; i++ {
s = append(s, i)
fmt.Println("Length:", len(s), "Capacity:", cap(s))
}
运行结果: