Go 的 slice 是值类型,其结构体包含指向底层数组的指针、len 和 cap 字段;传参修改生效是因为值中含指针,而非引用传递;append 等操作返回新 slice,需显式赋值,否则修改丢失。

Go 的 slice 不是引用类型,但它的结构体里存着指针
很多人以为 slice 是引用类型,所以传参修改能影响原值——这结论对,但理由错。它其实是值传递,只是这个“值”里包含一个指向底层数组的 ptr 字段。你可以把它理解成:一个轻量级的“数组视图”结构体(struct { ptr *T; len, cap int })。
常见错误现象:
— 在函数里用 append 后没返回新 slice,原变量长度没变
— 以为 slice 赋值是深拷贝,结果两个 slice 共享底层数组,改一个另一个也变
- 所有
slice操作(append、切片、copy)都只操作这个结构体字段,不自动复制底层数组 -
len和cap独立变化:比如s[1:3]可能len=2、cap=2(如果原cap刚好是 3),也可能cap=99(取决于原cap和起始偏移) - 当
append超出cap,Go 会分配新底层数组并复制数据,此时新slice的ptr指向新地址,和原slice彻底断开
为什么 append 后不赋值就失效?
因为 append 返回的是新的 slice 结构体(可能指向新内存,也可能还是原数组),而原变量没被更新。它不是像 Python 的 list.append() 那样就地修改。
使用场景:循环中累积元素、构建动态列表
- 必须显式接收返回值:
s = append(s, x),否则新增元素丢了 - 如果原
slice的cap不够,append分配新数组后,原变量仍指向旧内存,新数据根本不在那儿 - 性能影响:频繁小量
append可能触发多次扩容(2 倍增长策略),提前用make([]T, 0, n)预设cap更稳
共享底层数组导致的“幽灵修改”
只要两个 slice 的 ptr 指向同一块内存,且它们的索引范围有重叠,改一个就会静默影响另一个。这不是 bug,是设计使然,但极易误用。
常见错误现象:
— 从大 slice 切出多个子 slice,后续某个 append 触发扩容,其他子 slice 却还指着旧底层数组,读到脏数据或 panic
— 解析网络包时反复切片复用,结果前一次的数据残留在后续 slice 中
- 用
copy手动分离:newS := make([]byte, len(oldS)); copy(newS, oldS) - 或者用
append([]T(nil), oldS...)快速深拷贝(注意nil切片的cap是 0,强制新分配) - 调试时可用
unsafe查ptr地址对比,但生产代码别这么干
扩容策略和 cap 的实际表现
Go 的扩容不是固定倍数,而是分段策略:小容量翻倍,大容量增幅变小(比如从 1024 → 1280,不是 2048)。这意味着你不能假设 cap 一定是 2 的幂。
参数差异:
— make([]int, 5) → len=5, cap=5
— make([]int, 5, 10) → len=5, cap=10
— make([]int, 0, 10) → len=0, cap=10,这是最安全的预分配写法
- 扩容只发生在
append且len == cap时;slice本身不主动扩容 -
cap决定是否需要分配:哪怕你只追加 1 个元素,只要len == cap,就可能整块复制 - 用
reflect.Value.Cap()可在运行时查当前cap,但别在热路径用反射
底层数组的生命周期由所有引用它的 slice 共同决定,只要还有一个 slice 活着,数组就不会被 GC。这点容易被忽略——尤其在长期持有小 slice 却引用了超大底层数组时,会造成内存泄漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











