append返回新切片头是值传递语义的必然结果,因切片是值类型,append只能返回新状态;容量足够时复用原底层数组,不赋值则追加失效,必须显式接收返回值。

append 返回新切片头是值传递语义的必然结果
Go 中所有参数都按值传递,append 接收的只是原切片头(ptr、len、cap 三个字段)的一个副本。它无法修改调用方栈上的原始变量——哪怕你传的是指针,append 本身也不接收指针。所以它只能计算出一个新状态,并把整个新切片头返回。这不是设计选择,而是语言模型决定的:切片是值类型,不是引用类型。
容量足够时,新切片头可能复用原底层数组
常见误解是“返回新切片头 = 底层数组一定变了”,其实不然。只要 cap(s) - len(s) >= 新增元素个数,append 就会直接往原数组末尾写,仅更新返回切片的 len 字段,ptr 和 cap 通常不变。这时多个变量若基于同一高容量切片反复 append,就会意外共享内存——静默覆盖就发生在这里。
- 示例:
common := make([]int, 0, 12);连续三次s = append(common, 1,2,3),三次返回的s都指向同一块内存 - 调试技巧:打印
&s[0]地址,两次不等说明底层数组已搬迁;相等 ≠ 安全,只说明没扩容
不接收返回值会导致追加完全失效
写 append(s, x) 而不赋值,等于什么都没做。编译器会报 append(...) not used,运行时 s 的 len 不变,底层数组可能被写但没人能访问到新增部分。这是最常踩的坑,尤其在结构体字段或函数参数中。
- 结构体中:必须写
r.Slice = append(r.Slice, x),不能只调用append(r.Slice, x) - 函数内修改:要么返回新切片(
func extend(s []int, x int) []int { return append(s, x) }),要么用指针接收器(func (s *MySlice) Push(x int) { *s = append(*s, x) }) - 对
nil切片调用append是安全的,会自动分配底层数组,但依然要接返回值
底层数组搬迁的判断不能依赖 cap 变化
扩容后 cap 一定变大,但 cap 变了不一定代表搬迁——比如你手动 make([]int, 0, 100) 后又 make([]int, 0, 200),再 append,cap 变了但没搬迁。唯一可靠方式是比对 &s[0] 地址是否变化。这也是为什么保存 &s[i] 后还继续 append 极其危险:一旦搬迁,指针就悬垂了。
真正关键的不是“返回新头”这个动作,而是你是否意识到:每次 append 都可能让底层数组地址失效,而你手里的旧指针、旧切片头、旧长度,全都不可信了。











