Skip to main content

Go: slice 切片

📅 2026-02-05 ✏️ 2026-03-06 Inside Go CS GO
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#

  1. 如果要求的cap大于old.cap的两倍,则newcap = cap
  2. 否则:
  • 如果old.cap小于256,newcap = old.cap*2;
  • 否则,循环:newcap += (newcap + 1024) / 4 直到 newcap > cap, 大约有1.25倍增长
  1. 计算newcap比特数,分配内存p
  2. 计算old.len比特数lenmem
  3. 从old.array出移动lenmem个比特到p处

1.4 · slices包(依赖范型)#

https://pkg.go.dev/slices@go1.25.6

go doc slices
  1. 创建、构造
  2. 复制、容量调整
  3. 比较、相等性
  4. 搜索、查找
  5. 排序、极值
  6. 过滤、清理
  7. 修改、变化
  8. 迭代、分块

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

  1. https://go.dev/ref/spec#Slice_types
  2. https://go.dev/blog/slices-intro
  3. https://go.dev/blog/slices