这是青训营笔记的第三篇文章。
基本介绍
go 语言中的切片对标于其他编程语言中通俗意义上的“数组”. 切片中的元素存放在一块内存地址连续的区域,使用索引可以快速检索到指定位置的元素;切片长度和容量是可变的,在使用过程中可以根据需要进行扩容.
数据结构
type slice struct {
// 指向起点的地址
array unsafe.Pointer
// 切片长度
len int
// 切片容量
cap int
}
切片的类型定义如上,我们称之为 slice header,对应于每个 slice 实例,其中核心字段包括:
• array:指向了内存空间地址的起点. 由于 slice 数据存放在连续的内存空间中,后续可以根据索引 index,在起点的基础上快速进行地址偏移,从而定位到目标元素
• len:切片的长度,指的是逻辑意义上 slice 中实际存放了多少个元素
• cap:切片的容量,指的是物理意义上为 slice 分配了足够用于存放多少个元素的空间. 使用 slice 时,要求 cap 永远大于等于 len
可以看出,slice与cpp中的vector十分相似,通过cap和len(size)的双重限制,在实现动态扩容的基础上,实现了边界检查与内存安全实话说还是很不安全。因此在学习slice这一基本数据类型,我们只需要在对vector的认识上加上go语言特有机制即可。
初始化
下面先来介绍下切片的初始化操作:
• 声明但不初始化
下面给出的第一个例子,只是声明了 slice 的类型,但是并没有执行初始化操作,即 s 这个字面量此时是一个空指针 nil,并没有完成实际的内存分配操作.
var s []int
• 基于 make 进行初始化
make 初始化 slice 也分为两种方式, 第一种方式如下:
s := make([]int,8)
此时会将切片的长度 len 和 容量 cap 同时设置为 8. 需要注意,切片的长度一旦被指定了,就代表对应位置已经被分配了元素,尽管设置的会是对应元素类型下的零值.
第二种方式,是分别指定切片的长度 len 和容量 cap,代码如下:
s := make([]int,8,16)
如上所示,代表已经在切片中设置了 8 个元素,会设置为对应类型的零值;cap = 16 代表为 slice 分配了用于存放 16 个元素的空间. 需要保证 cap >= len. 在 index 为 [len, cap) 的范围内,虽然内存空间已经分配了,但是逻辑意义上不存在元素,直接访问会 panic 报数组访问越界;但是访问 [0,len) 范围内的元素是能够正常访问到的,只不过会是对应元素类型下的零值.
• 初始化连带赋值
初始化 slice 时还能一气呵成完成赋值操作. 如下所示:
s := []int{2,3,4}
这样操作的话,会将 slice 长度 len 和容量 cap 均设置为 3,同时完成对这 3 个元素赋值.
下面展示切片初始化的源代码
func makeslice(et *_type, len, cap int) unsafe.Pointer {
// 根据 cap 结合每个元素的大小,计算出消耗的总容量
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// 倘若容量超限,len 取负值或者 len 超过 cap,直接 panic
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
// 走 mallocgc 进行内存分配以及切片初始化
return mallocgc(mem, et, true)
}
上述方法核心步骤是
• 调用 math.MulUintptr 的方法,结合每个元素的大小以及切片的容量,计算出初始化切片所需要的内存空间大小
• 倘若内存空间超限,则直接抛出 panic
• 调用位于 runtime/malloc.go 文件中的 mallocgc 方法,为切片进行内存空间的分配
截取
我们可以修改 slice 下标的方式,进行 slice 内容的截取,形如 s[a:b] 的格式,其中 a b 代表切片的索引 index,左闭右开,比如 s[a:b] 对应的范围是 [a,b),代表的是取切片 slice index = a ~ index = b-1 范围的内容.
此外,这里我聊到的 a 和 b 是可以缺省的:
• 如果 a 缺省不填则默认取 0 ,则代表从切片起始位置开始截取. 比如 s[:b] 等价于 s[0:b]
• 如果 b 缺省不填,则默认取 len(s),则代表末尾截取到切片长度 len 的终点,比如 s[a:] 等价于 s[a:len(s)]
• a 和 b 均缺省也是可以的,则代表截取整个切片长度的范围,比如 s[:] 等价于 s[0:len(s)]
在对切片 slice 执行截取操作时,本质上是一次引用传递操作,因为不论如何截取,底层复用的都是同一块内存空间中的数据,只不过,截取动作会创建出一个新的 slice header 实例.