slice 是 golang 中最长使用的基础数据结构之一。区别于数组,slice 是一个引用类型。
基础使用
声明
// int 可以替换为任意类型标识 |
访问数据
item1 := slice[0] // 下表取值 下表从0开始 |
操作
拼接
- append 追加元素
newSlice := append(slice, item1, item2, ...) // 追加元素 |
- contact 拼接切片
go1.22 新特性,用来简化多个切片的拼接。slices 包中还有很多有用的函数,读者们可以自行探索。
// 类似append(slice1, append(slice2, slice3)) |
裁剪
操作的本质是创建新的 slice 头,公用存储空间,并不会申请新的存储空间。没有发生扩容的情况下,对裁剪下来的切片进行数据修改会影响原切片。这里的操作结果是不可预测的,使用时应当充分小心。
slice1[start:end] // 裁剪从start开始,到end结束不包含end下标的元素 裁剪后的cap为原cap-start |
用例:移除切片中的元素
slice1[1:] // 移除首个元素 |
拷贝
newSlice := slice // 浅拷贝,仅靠被头信息 |
传参
// 函数签名 func test([]int) |
如果想要使用函数处理后的 slice 可以有以下几个方法
- 将 slice 作为参数返回 推荐
- slice 作为引用类型返回只会拷贝头部信息,数据量并不大。
- 使用 slice 指针进行传参
- 如果使用指针传递会发生内存逃逸,虽然扩容也可能触发逃逸,但在大部分情况下作为参数传递是更高效的
- 保证 slice 的容量足够,不会在函数中发生扩容
- 大多数情况下我们无法正确的估计 slice 的容量,给出冗余量又会造成浪费,极端情况下还会发生逃逸
panic
slice 在以下两种情况下会触发 panic
- 下标超出限制
- 使用下标访问 nil 切片
- 访问下标超过 len 所限定范围
- make 时 cap 过大 实际编码很少遇到
- 32 位系统设置的值超过 int32 的最大值
- 所需分配空间超过计算机寻址空间
源码分析
源码版本 go1.22
源码路径:src\runtime\slice.go
slice 底层结构
type slice struct { |
make 逻辑
- makeslice
- 参数校验
- mallocgc 申请内存资源 mem:申请的空间大小 et:元素类型 true:初始化位零值
//go:linkname makeslice |
扩容逻辑
- growslice
oldPtr: 老切片的 array 锁指向的地址
newLen: 新的切片长度 oldLen + num
oldCap: 老切片的容量
num:要添加的元素长度
et:元素类型
- 参数校验 处理参数异常,过滤掉不需要新空间的情况
- 计算所需要的新空间大小
- nextslicecap 确定新的 cap 大小
- 空间计算使用了三种方法优化计算 主要是规避掉除法计算 使用移位替代乘法
- 申请新的内存空间
- 如果元素类型不包含指针 清理尾部不会写数据的 cap-len 长度的空间
- 如果包含指针,处理和 gc 相关的指针映射
- 拷贝老切片的数据到新的切片
//go:linkname growslice |
- nextslicecap 返回新切片的容量
计算逻辑逐级匹配
- 如果新切片超过老切片容量的两倍则使用新切片长度作为容量
- 如果老切片容量小于阈值 256 返回两倍老切片容量
- 使用公式
newcap += (newcap + 3*threshold) >> 2计算知道 newcap 大于 newLen, 这是翻倍扩容和 1.25 倍扩容间的平滑处理 - 如果发生移除则返回 newLen
// nextslicecap computes the next appropriate slice length. |
- 本文作者: Tiny Beer
- 本文链接: https://tinybeer.github.io/2024/06/07/slice详解/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!
