Go语言切片原理初探|青训营

101 阅读4分钟

这是青训营笔记的第三篇文章。

基本介绍

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 实例.