append 不会修改原 slice 变量,但可能复用底层数组;因 go 值传递,s 副本传入,返回新 slice,其底层数组是否复用取决于 len(s)+1 ≤ cap(s)。

append 不会修改原 slice 变量,但可能复用底层数组
调用 append(s, x) 后,s 本身不会变——因为 Go 是值传递,传入的是 slice 结构体(含指针、len、cap)的副本。真正变化的是返回的新 slice:它可能指向原底层数组,也可能指向新分配的数组。
关键判断依据是容量:len(s) + 1 成立时,<code>append 直接在原数组末尾写入;否则触发扩容,分配新数组并复制旧数据。
- 不扩容 → 新 slice 和原 slice 共享底层数组 → 修改其中一个的元素,可能影响另一个(尤其通过切片表达式访问时)
- 扩容 → 新 slice 指向全新底层数组 → 隔离性好,但有复制开销
子切片 + append 导致父切片被意外覆盖
这是最典型的副作用场景:从一个切片派生出子切片(如 s2 := s1[1:2]),再对子切片调用 append。只要 s2 的容量够用,新元素就会写进 s1 的底层数组里,直接改掉 s1 的内容。
例如:
s1 := []int{1, 2, 3}
s2 := s1[1:2] // len=1, cap=2
s3 := append(s2, 99)
// 此时 s1 变成 [1, 2, 99],不是 [1, 2, 3]
- 根本原因:子切片的
cap是从其起始位置算到底层数组末尾,常比看起来大得多 - 验证方式:
fmt.Printf("cap(s2)=%d, len(s2)=%d\n", cap(s2), len(s2)) - 别依赖“只取了一小段”就认为安全——要看
cap,不是len
多次 append 同一基础 slice 产生数据覆盖
当多个变量都基于同一个初始 slice(比如都用 make([]int, 0, 10) 创建),又各自调用 append,且都没触发扩容,它们最终可能写到同一块内存的不同偏移上,相互覆盖。
典型例子:
s := make([]int, 0, 10) s1 := append(s, 1) s1 = append(s1, 2) s2 := append(s, 3) // 这里复用 s 的底层数组,从索引 0 开始写 s2 = append(s2, 4) // 写到索引 1 → 覆盖了 s1 写入的 2
- 输出
s1是[1, 4],不是预期的[1, 2] - 问题根源在于:所有
append都没扩容,全挤在同一个底层数组里 - 规避方法:每次操作前用
slices.Clone(s)(Go 1.21+)或copy(dst, src)显式隔离
如何安全地避免副作用
没有银弹,但有明确可执行的动作:
- 需要独立副本时,不用
append做“复制”,而用slices.Clone(s)或手动make+copy - 函数接收 slice 参数并要
append时,除非文档明确约定“允许修改原底层数组”,否则应先克隆 - 调试时怀疑共享问题?打印地址:
fmt.Printf("%p", s)—— 注意这不是 slice 结构体地址,而是底层数组首地址 - 初始化时预留足够
cap并控制使用节奏,不如直接接受“不确定是否扩容”,一律按需克隆更可靠
底层数组是否复用,取决于运行时容量和元素数量,不是代码字面能一眼看出的。靠人脑推演容易漏,靠工具验证又滞后——最省心的做法,是在任何可能暴露给外部或并发使用的 slice 上,把克隆当成默认动作。











