重要
Slice 就三件事需要记牢:
- 底层结构:
slice header = {ptr, len, cap},值类型,赋值/传参复制 header 但共享底层数组。 - 共享与脱离:
s[i:j] 共享底层数组;append 触发扩容后脱离原数组。 - 扩容两把尺子:需求大小(> 2×cap?)与切片大小(≥ 256?),决定 “精确分配 / 翻倍 / 1.25x 渐进” 三选一。
1. 声明与创建
三种声明方式:
| 方式 | 代码 | len | cap | 是否 nil | 备注 |
|---|
| 零值 | var s []int | 0 | 0 | 是 | s == nil 为 true |
| 字面量 | s := []int{1, 2, 3} | 3 | 3 | 否 | 最常用 |
| make | s := make([]T, len, cap) | len | cap | 否 | 预分配容量,减少扩容 |
1.1 nil vs 空切片
行为一致(len、cap、range、append 都能正常工作),仅 JSON 序列化不同:
1
2
3
4
5
| var nilSlice []int
emptySlice := []int{}
json.Marshal(nilSlice) // "null"
json.Marshal(emptySlice) // "[]"
|
对外 API 建议返回空切片而非 nil。
2. 核心机制
Slice 值本质是一个三元组:
1
2
3
4
5
| type slice struct {
ptr unsafe.Pointer // 指向底层数组
len int // 当前长度
cap int // 从 ptr 起可用容量
}
|
Slice 是值类型:赋值/传参复制的是 header,ptr 仍指向同一个底层数组。
1
2
3
4
| a := []int{1, 2, 3}
b := a // 复制 header,共享底层数组
b[0] = 99
fmt.Println(a) // [99 2 3]
|
2.2 子切片共享底层
s[i:j] 与原切片共享底层数组,改元素互相可见:
1
2
3
4
| a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3],cap=4
b[0] = 99
fmt.Println(a) // [1 99 3 4 5]
|
避免共享:完整切片表达式 s[low:high:max] 限制 cap,后续 append 必扩容并分配新数组:
1
2
| b := a[1:3:3] // len=2, cap=2
b = append(b, 10) // 扩容,脱离 a 的底层数组
|
2.3 append 与扩容脱离
append 在容量足够时复用底层数组,容量不足时分配新数组并拷贝:
1
2
3
4
5
6
7
| a := make([]int, 0, 2)
a = append(a, 1, 2)
b := a // 共享底层数组
a = append(a, 3) // a 扩容,指向新数组
b[0] = 99 // b 仍指向旧数组
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [99 2]
|
必须接收返回值:
1
2
| append(s, 1) // 错:返回值被丢弃
s = append(s, 1) // 对
|
2.4 copy 不扩容
copy(dst, src) 复制 min(len(dst), len(src)) 个元素,不会扩展 dst:
1
2
3
| dst := make([]int, 2)
n := copy(dst, []int{1, 2, 3, 4})
// dst = [1 2], n = 2
|
3. 扩容机制
3.1 触发时机
append 时,如果 预期的新长度(len + 追加元素数) > cap,则触发扩容。
1
2
3
| s := make([]int, 0, 2)
s = append(s, 1, 2) // len=2, cap=2,未扩容
s = append(s, 3) // 预期 len=3 > cap=2 → 扩容
|
3.2 两把尺子
Go 1.18+ 扩容策略由两个维度决定:
| 尺子 | 判断 | 含义 |
|---|
| 需求大小 | 所需容量 > 2 × 原容量? | 一次追加是 “大幅跳跃” 还是 “小幅增长” |
| 切片大小 | 原容量 ≥ 256? | 当前是 “小切片” 还是 “大切片” |
三种情况:
| 情况 | 条件 | 策略 |
|---|
| 需求大 | 需求 > 2×cap | 精确分配到需求容量 |
| 需求小 + 小切片 | 需求 ≤ 2×cap 且 cap < 256 | 翻倍(2x) |
| 需求小 + 大切片 | 需求 ≤ 2×cap 且 cap ≥ 256 | 1.25x 渐进 |
一句话:需要大直接给;需要小 → 小切片翻倍,大切片 1.25x 渐进。批量 append 只算一次扩容。
3.3 大切片的平滑公式
大切片(cap ≥ 256)采用循环公式,增长系数从 2x 平滑降到 1.25x:
1
2
3
4
| newcap = oldcap
for newcap < expectedCap {
newcap += (newcap + 3*256) / 4
}
|
即每轮 newcap = (5 × oldcap + 3 × 256) / 4,循环直到满足需求容量。增长系数随 oldcap 变化:

为什么要循环
一次迭代的增长系数在 1.25~2 之间。当 oldcap 较小时,单次迭代未必够。示例:
1
2
3
4
| oldcap=256, expectedCap=800
第 1 轮:newcap = (5×256 + 3×256)/4 = 512 (2x)
第 2 轮:newcap = (5×512 + 3×256)/4 = 832 (≈1.625x)≥ 800 ✓
|
每轮增长系数变化(2.0 → 1.625 → … → 1.25),一轮不够就再来一轮。
3.4 内存对齐
上述公式算出的 newcap 只是目标容量。Go 运行时分配前会做内存对齐:
| 步骤 | 操作 |
|---|
| 1 | 计算目标容量 newcap |
| 2 | 结合元素类型大小,算出所需字节数 |
| 3 | 向上取整到 mspan 规格(8、16、48 字节等) |
| 4 | 最终 cap = 对齐后字节数 ÷ 元素大小 |
最终 cap 通常略大于计算值,最多到下一个 mspan 规格。
3.5 Go 1.18 前后差异
| 版本 | 小切片 | 大切片 |
|---|
| 1.17 及以前 | cap < 1024 翻倍 | cap ≥ 1024 一律 1.25x |
| 1.18+ | cap < 256 翻倍 | cap ≥ 256 平滑过渡 2x → 1.25x |
改动动机:1024 之前一律翻倍,大容量切片浪费严重;新算法在 256 之后就启动平滑过渡,更省内存。
3.6 预分配避免扩容
1
2
3
4
5
6
7
8
9
10
11
| // 差:多次扩容 + 拷贝
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i)
}
// 好:一次分配到位
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i)
}
|
4. 删除
Fast版:改变顺序(O(1))
将最后一个元素移到被删位置,不保持原顺序:
1
2
3
4
5
6
| a := []string{"A", "B", "C", "D", "E"}
i := 2 // 删除 "C"
a[i] = a[len(a)-1] // 末位移到 i
a = a[:len(a)-1] // 截断
// [A B E D]
|
Slow版:保持顺序(O(n))
被删元素之后的元素全部前移一位:
1
2
3
4
5
6
| a := []string{"A", "B", "C", "D", "E"}
i := 2 // 删除 "C"
copy(a[i:], a[i+1:]) // C 被 D 覆盖,D 被 E 覆盖
a = a[:len(a)-1] // 截断
// [A B D E]
|
泛型版本(Go 1.18+)
1
2
3
| func Delete[T any](s []T, i int) []T {
return append(s[:i], s[i+1:]...)
}
|
5. 插入
1
2
3
4
5
6
7
| // 在位置 i 插入元素 x
s = append(s[:i], append([]int{x}, s[i:]...)...)
// Go 1.17+ 可以用 copy 避免临时切片
s = append(s, 0) // 扩展一个位置
copy(s[i+1:], s[i:]) // i 之后的元素后移
s[i] = x
|
1
2
3
4
5
6
| func Insert[T any](s []T, i int, x T) []T {
s = append(s, *new(T)) // Go 1.21: clear 更好
copy(s[i+1:], s[i:])
s[i] = x
return s
}
|
6. 过滤
1
2
3
4
5
6
7
8
9
| // 就地过滤,保留满足条件的元素
n := 0
for _, v := range s {
if predicate(v) {
s[n] = v
n++
}
}
s = s[:n]
|
1
2
3
4
5
6
7
8
9
10
| // 过滤偶数
nums := []int{1, 2, 3, 4, 5, 6}
n := 0
for _, v := range nums {
if v%2 == 0 {
nums[n] = v
n++
}
}
nums = nums[:n] // [2 4 6]
|
7. 深拷贝
1
2
3
| src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src) // 深拷贝
|
8. 清空(Go 1.21+)
1
| clear(s) // 所有元素置零值,len 不变
|
旧版本等价写法:
1
2
3
| for i := range s { s[i] = T{} }
// 或
s = s[:0] // 重置 len 为 0,保留底层数组和 cap
|
9. 常见陷阱
9.1 range 中的值拷贝
1
2
3
4
5
6
7
8
9
10
| s := []User{{"A"}, {"B"}}
for _, u := range s {
u.Name = "X" // 修改的是副本,不影响 s
}
fmt.Println(s) // [{A} {B}]
// 正确做法
for i := range s {
s[i].Name = "X"
}
|
9.2 子切片导致的内存泄漏
1
2
3
4
5
6
7
8
| // 大切片的一个小窗口,但底层大数组无法 GC
big := readMillionLines()
first10 := big[:10]
// 即使只用 10 行,百万行的底层数组依然存在
// 修复:拷贝需要的部分
first10 := make([]Line, 10)
copy(first10, big[:10])
|
9.3 共享数组导致的意外修改
传入函数的 slice 若容量足够,函数内的 append 会写入原底层数组:
1
2
3
4
5
6
7
| func appendOne(s []int) {
s = append(s, 999) // 若原 cap 够用,直接写原底层数组
}
a := make([]int, 2, 10)
appendOne(a)
fmt.Println(a[:3]) // [0 0 999]
|
10. 总结
| 操作 | 代码 | 注意 |
|---|
| 声明 | var s []T / []T{...} / make([]T, len, cap) | 三选一 |
| 追加 | s = append(s, x) | 必须接收返回值 |
| 删除(保序) | copy(s[i:], s[i+1:]); s = s[:len(s)-1] | O(n) |
| 删除(不保序) | s[i] = s[len(s)-1]; s = s[:len(s)-1] | O(1) |
| 子切片 | s[low:high:max] | 三参数限制 cap 避免共享 |
| 深拷贝 | dst := make([]T, len(src)); copy(dst, src) | |
| 过滤 | 双指针法 | O(n),就地 |
| 清空 | clear(s) / s = s[:0] | Go 1.21+ / 旧版 |
参考链接