深入解析Go中Slice底层实现
一、Slice的底层数据结构
切片是 Go 中的一种基本的数据结构,是对底层数组的一个动态、灵活的视图。使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。
在golang runtime中,slice的实现并不是一个指针,而是一组结构体:
1 | // runtime/slice.go 中的定义 |
切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C++ 中的 Vector 类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。
二、内存布局
假设我们有
1 | arr := [6]int{0, 1, 2, 3, 4, 5} |
则内存布局如下:
1 | 底层数组 arr: |
cap = len(arr) - start_index = 6 - 1 = 5
三、关键操作的底层行为
1. 创建切片(字面量或 make)
1 | s := []int{10, 20, 30} |
- Go 会在堆上分配一个底层数组
[10, 20, 30] - 创建一个 slice 结构体:
array→ 指向该数组首地址len = 3cap = 3
1 | s := make([]int, 2, 5) |
- 分配一个长度为 5 的底层数组(初始化为 0)
- slice 结构体:
array→ 指向数组首地址len = 2cap = 5
2. 切片操作(s[i:j])
1 | a := []int{0, 1, 2, 3, 4, 5} |
b.array = a.array + 1 * sizeof(int)(指针偏移)b.len = 4 - 1 = 3b.cap = a.cap - 1 = 6 - 1 = 5
不复制数据!只是调整指针和长度/容量。
验证代码:
1 | package main |
Output:
1 | a 地址: 0xc0000140c0, len=6, cap=6 |
b的起始地址比a大 8 字节(64 位系统,int 占 8 字节),说明指针偏移。
3. append 的底层逻辑
append 的伪代码逻辑如下:
1 | func append(slice []T, elements ...T) []T { |
扩容策略(Go 1.18+):
- 如果
cap < 1024:新容量 ≈ 2 * 旧容量 - 如果
cap >= 1024:每次增加约 1/4 容量(直到满足需求)
实际策略更复杂,考虑内存对齐等。
示例:观察扩容
1 | package main |
Output:
1 | 初始: len=0, cap=0 |
可见容量按 1→2→4→8 增长(指数增长)。
4. copy 的底层行为
1 | dst := make([]int, 3) |
- 底层调用
memmove(或类似)复制min(len(dst), len(src))个元素 - 不改变 dst 的 len/cap,只改内容
四、使用 unsafe 观察 slice 结构(高级)
⚠️ 仅用于学习,生产代码避免使用
unsafe
1 | package main |
Output:
1 | slice s: |
ptr与&s[0]地址一致,验证了 slice 的指针指向底层数组首元素。
引用
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 全之の博客!
