1161 字
约 3 分钟
5
S09-J01 · 切片底层:cap 增长、共享与内存泄漏
适用读者:被"append 之后原切片没变/被覆盖"搞晕过的人。
前置:S09-B04。
实测环境:Go 1.22.2(2026-11-05)。
覆盖旧文:—(E 驱动;B04 的机制在此深化)
覆盖扩展:E-S09-010(三索引切片)、E-S09-011(copy切断共享)。
上一篇:S09-X01 · 学习路线与待补充清单
下一篇:S09-J02 · 字符串与 UTF-8。
1. 一句话结论
切片的三字段 {ptr, len, cap} 决定了三件事:能不能看到元素(len)、能不能原地追加(cap)、改了会不会影响别人(ptr 是否共享)。
| 你想做的事 | 看哪个字段 | 结果 |
|---|---|---|
| 读第 i 个元素 | len |
越界 panic |
append 是否重新分配 |
cap - len |
够 → 原地;不够 → 新数组 |
| 修改是否影响原数组 | ptr 是否相同 |
相同 → 影响 |
2. append 的两种命运
append(s, x)
├─ len < cap → 原地写入 s[len],len++,ptr 不变(共享!)
└─ len == cap → 新数组(容量翻倍),拷贝旧数据,ptr 变(独立)
第二种情况会"悄悄覆盖原数组"(当多个切片共享且容量足够时):
a := []int{1, 2, 3, 4}
b := a[:2] // len=2, cap=4(共享)
b = append(b, 99) // len<cap → 原地写 a[2] = 99
fmt.Println(a) // [1 2 99 4] ← 被改了!
这条比"改下标"更隐蔽:你只是 append,却改了别的切片。
3. 三索引切片:给容量上锁(E-S09-010)
a := []int{1, 2, 3, 4, 5}
full := a[1:3] // len=2 cap=4
limited := a[1:3:3] // len=2 cap=2 ← 上限设为 3
实测输出:
full len/cap: 2 4 limited len/cap: 2 2
然后 append 到 limited:
limited = append(limited, 99)
// 实测:limited = [2 3 99] a = [1 2 3 4 5] ← a 未受影响
| 写法 | cap |
append 后是否影响原数组 |
|---|---|---|
a[1:3] |
4 | 会(原地写) |
a[1:3:3] |
2 | 不会(超 cap → 新数组) |
适用场景:把切片传给不可信代码(如插件、回调)时,用三索引锁住容量,防止对方 append 覆盖你的数据。
4. copy:彻底切断共享(E-S09-011)
c := make([]int, 2)
copy(c, a[1:3])
c[0] = 77
// 实测:c = [77 3] a = [1 2 3 4 5]
copy 是唯一"复制数据"的标准做法(三索引只是限制容量,ptr 仍然共享)。
| 需求 | 做法 |
|---|---|
| 只是防 append 覆盖 | 三索引 a[l:h:h] |
| 需要完全独立 | copy(make([]T, len), s) |
| 需要"快照"后原数组可继续被改 | 必须 copy |
5. 内存泄漏:小切片持有大数组
func firstN(big []byte, n int) []byte {
return big[:n] // 只用了 n 字节,但底层数组是整个 big
}
问题:返回的切片持有 big 的 ptr → 整个大数组无法被 GC(即使你只用了前 n 字节)。
修法:
func firstN(big []byte, n int) []byte {
out := make([]byte, n)
copy(out, big[:n])
return out // 只持有 n 字节
}
| 场景 | 是否真的泄漏 |
|---|---|
| 长期存活的缓存/全局变量持有小切片 | ✅ 是真泄漏 |
| 短生命周期(函数返回后很快释放) | ⚠️ 影响有限 |
判据:"这个小切片会存活很久吗?" 会 → copy 一份。
6. 五个误区
| 误区 | 真相 |
|---|---|
"append 一定复制" |
未超 cap 时原地写,可能覆盖原数组 |
"b := a[:n] 是独立副本" |
共享底层数组 |
| "三索引切片会复制数据" | 只限制 cap,ptr 仍共享 |
| "小切片一定省内存" | 可能持有整个大数组(§5) |
"cap 只在 append 时有用" |
也决定"能不能原地写",进而决定副作用 |
7. 自检
append何时会覆盖原数组?为什么?a[1:3:3]的cap是多少?为什么能阻止覆盖?- 三索引与
copy的区别? - 什么情况下小切片会导致内存泄漏?怎么修?
- 如何判断两个切片是否共享底层数组?(提示:比较
&s[0])
8. 下一篇
S09-J02 · 字符串与 UTF-8:byte、rune 与 O(n) 陷阱
切片讲完,下一篇深入字符串:为什么按字节截断会切坏中文、range 的索引为何是字节偏移、以及安全的截断写法(含实测的"非法 UTF-8"输出)。
本篇属于系列《S09 · Go 语言》。扩展点 E-S09-010/011。三索引与 copy 行为为 Go 1.22.2 实测。
S09-J01 · 切片底层:cap 增长、共享与内存泄漏
https://www.clxhxhhr.top/posts/5306/ 评论
0 条
还没有评论,先写一条吧。