1 前言
去年校招入职字节跳动从Java转Go已经有大半年了,说实话Go上手还是很容易的,而我期间一直在写业务相关的需求,对Go的一些底层数据结构的了解还是不够深入的,难得春节假期放长假,那么就以博客的形式来记录下Go数据结构的学习吧
2 切片简介
切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。
3 切片的数据结构
切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。
Slice 的数据结构定义如下:
type slice struct { array unsafe.Pointer len int cap int }
可见slice结构体由三部分组成:
- array 表示一个指向数组的指针
- len 表示切片的长度
- cap 表示切片的最大容量
简单了解了切片的数据结构后,我们来看看切片初始化的几种方式,后续还会对切片的内部实现做更近深入的介绍。
4 切片初始化的几种方式
Go中有几种方法可以创建和初始化切片。是否能提前知道切片需要的容量通常会决定要如何创建切片。
3.1 nil 切片
var slice []int64
在Go里,nil 切片是很常见的创建切片的方法,Go不会为nil切片分配任何空间。
3.2 空切片
// 使用 make 创建空的整型切片 slice := make([]int, 0) // 使用切片字面量创建空的整型切片 slice := []int{}
在Go里,空切片在底层数组包含 0个元素,也没有分配任何存储空间。
不管是使用 nil 切片还是空切片,对其调用内置函数 append、len 和 cap 的返回结果都是一样的。
3.3 nil 切片与空切片的区别
由上面两个示意图可以知道,空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它也没有分配任何内存空间,即底层数组中包含0个元素。
3.4 make和切片字面量
一种创建切片的方法是使用内置的 make 函数。当使用 make 时,需要传入一个参数,指定切片的长度:
// 创建一个字符串切片 // 其长度和容量都是5个元素 slice := make([]string, 5)
如果只指定长度,那么切片的容量和长度相等。也可以分别指定长度和容量:
// 创建一个整型切片 // 其长度为 3 个元素,容量为 5 个元素 slice := make([]int, 3, 5)
分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的数组元素。切片可以访问 3 个元素,而底层数组拥有 5 个元素。剩余的 2 个元素可以在后期操作中合并到切片,可以通过切片访问这些元素。如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多余容量的元素。
不允许创建容量小于长度的切片:
// 创建一个整型切片 // 使其长度大于容量 slice := make([]int, 5, 3) // Compiler Error: // len larger than cap in make([]int)
另一种常用的创建切片的方法是使用切片字面量,这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定。
// 创建一个整型切片 // 其长度和容量都是 3 个元素 slice := []int{10, 20, 30}