Go: slice 切片
Backlinks (1)
Backlinks (1)
go version # go version go1.25.6
1 · slice 切片#
A slice is a descriptor for a contiguous segment of an underlying array and provides access to a numbered sequence of elements from that array.
可以把切片看作指向某块连续区域,其底层是一个数组,有长度、有容量。
SliceType = ”[” ”]” ElementType .
长度不是类型的一部分。
其长度不像数组(在编译期就固定了),可以在运行时改变。
访问类似数组,可以通过下标访问。
多个切片,可以指向同一个数组(可能导致问题);这里与数组不同(不同数组对应不同的空间)。
底层数组是支持扩容的(可能导致问题):追加append扩容,但是reslice超过长度不扩容
1.1 · 初始化
// 1. literal:
var slice1 []int
slice2 := []int{1, 2, 3}
// 2. make: make([]T, length, capacity)
slice3 := make([]int, 10)
slice4 := make([]int, 10, 100)
// 3. slice expression:
slice5 := slice3[:50]
1.2 · 相关操作
var slice []int
// 1. 下标访问、写入
e := slice[1]
slice[1] = 33
// 2. 遍历
for i, e := range slice {
}
// 3. 长度、容量
len := len(slice)
cap := cap(slice)
// 4. 追加
slice = append(slice, 123)
// 5. 复制
var dstSlice []int
n := copy(dstSlice, slice)
// 6. 再切片
slice2 := slice[:] // 左闭右开
// slice3 := slice[low:high:max] // 「截出一段」并限制其容量,避免后续 append 改到原切片后面的元素(此时会重新分配)
// 7. 清空(设置为零值)
clear(slice2)
1.3 · 扩容:growslice#
- 如果要求的cap大于old.cap的两倍,则newcap = cap
- 否则:
- 如果old.cap小于256,newcap = old.cap*2;
- 否则,循环:newcap += (newcap + 1024) / 4 直到 newcap > cap, 大约有1.25倍增长
- 计算newcap比特数,分配内存p
- 计算old.len比特数lenmem
- 从old.array出移动lenmem个比特到p处
1.4 · slices包(依赖范型)#
go doc slices
- 创建、构造
- 复制、容量调整
- 比较、相等性
- 搜索、查找
- 排序、极值
- 过滤、清理
- 修改、变化
- 迭代、分块
NOTE: 带Func后缀,提供自定义比较/过滤的泛化版本
NOTE: 与迭代器之间的交互
1.5 · Inside Slice#
编译期: src/cmd/compile/internal/types/type.go:NewSlice src/cmd/compile/internal/types/type.go:Slice 切片的操作基本都是在编译期间完成的,除了访问切片的长度、容量或者其中的元素之外,编译期间也会将包含 range 关键字的遍历转换成形式更简单的循环。
运行时: src/reflect/value.go:SliceHeader src/cmd/compile/internal/gc/ssa.go:state.append src/runtime/slice.go:growslice
go tool compile -S main.go >main.s
go build -gcflags "-N -l -S" main.go
package opslicemake
func newSlice() []int {
arr := [3]int{1, 2, 3}
slice := arr[0:1]
return slice
}
// GOSSAFUNC=newSlice 得到一系列 SSA 中间代码,其中 slice := arr[0:1] 语句在 “decompose builtin” 阶段对应的代码如下所示:
// 使用下标初始化切片不会拷贝原数组或者原切片中的数据,它只会创建一个指向原数组的切片结构体,所以修改新切片的数据也会修改原切片。
1.5.1 · 初始化
// === 1. 字面量
// src/cmd/compile/internal/gc/sinit.go:slicelit
// 在编译期间展开
var vstat [3]int // 1. 根据切片中的元素数量,推断底层数组的大小,并创建一个数组
vstat[0] = 1 // 2. 将字面量元素存储到初始化的数组中
vstat[1] = 2
vstat[2] = 3
var vauto *[3]int = new([3]int) // 3. 创建一个同样指向 [3]int 类型的数组指针
*vauto = vstat // 4. 将静态存储区的数组 vstat 赋值给 vauto 指针所在的地址
slice := vauto[:] // 5. 通过 [:] 操作获取一个底层使用 vauto 的切片
// 6. 总结:先创建数组,再进行切片操作
// === 2. make函数
// src/cmd/compile/internal/gc/typecheck.go:typecheck1
// src/cmd/compile/internal/gc/walk.go:walkexpr
slice4 := make([]int, 10, 100)
// 1. 向 make 函数传入切片类型、大小、以及容量(可选)
// 2. 依据下面两个条件转换 OMAKESLICE 类型的节点
// - 切片的大小和容量是否足够小
// - 切片是否发生了逃逸,最终在堆上初始化
// 2.1 当切片发生逃逸或者非常大时,运行时需要 runtime.makeslice 在堆上初始化切片
// 2.2 如果当前的切片不会发生逃逸并且切片非常小的时候,1)新建数据,2)下标获取切片,这两部分操作都会在编译阶段完成
// runtime.makeslice(内存空间=切片中元素大小×切片容量) -> runtime.mallocgc(遇到了比较小的对象会直接初始化在 Go 语言调度器里面的 P 结构中,而大于 32KB 的对象会在堆上初始化)
// 3. 总结:可能在栈上/堆上(逃逸了)分配
// === 3. 切片表达式
逃逸分析: 分析数据应该放堆上还是栈上
1.5.2 · 扩容
如果期望容量大于当前容量的两倍就会使用期望容量;
如果当前切片的长度小于 1024 就会将容量翻倍;
如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
src/runtime/msize.go:roundupsize 仅会确定切片的大致容量,下面还需要根据切片中的元素大小对齐内存,当数组中元素所占的字节大小为 1、8 或者 2 的倍数时,运行时会使用如下所示的代码对齐内存
src/cmd/compile/internal/gc/walk.go:copyany 不是在运行时调用:
n := len(a)
if n > len(b) {
n = len(b)
}
if a.ptr != b.ptr {
memmove(a.ptr, b.ptr, n*sizeof(elem(a)))
}
在运行时: 使用 runtime.slicecopy 替换运行期间调用的 copy
1.6 · 一些坑
大切片上执行拷贝操作时一定要注意对性能的影响
https://stackoverflow.com/questions/30525184/array-vs-slice-accessing-speed