Go 的 append 不赋值就等于没追加,因它返回新切片而非就地修改;扩容策略分段(<1024 时翻倍,≥1024 时增 25%),无需手动模拟;底层数组共享需警惕悬垂指针与意外修改。

append 不赋值就等于没追加
Go 的 append 返回新切片,不是就地修改——它返回的是一个包含新 ptr、len、cap 的值。原变量不会自动更新。
- 常见错误现象:
append(s, x)写完不接返回值,fmt.Println(s)还是旧长度,编译器甚至会警告 “result of append not used” - 正确写法必须显式赋值:
s = append(s, x)或s = append(s, 1, 2, 3) - 函数内想让调用方看到变化,得返回:
func add(s []int, x int) []int { return append(s, x) },不能靠“传指针改切片头”来绕过(切片本身已是值类型)
扩容不是翻倍,但你不用自己算
Go 的 append 扩容策略是分段的:当当前 cap < 1024 时,新 cap = cap * 2;≥1024 后,新 cap = cap + cap/4(约 1.25×),再向上取整到内存对齐边界。
- 你不需要、也不应该手动模拟这个逻辑——自定义扩容(比如每次 +20)极易出错,且性能更差(实测慢 5–10 倍)
- 判断是否真扩容,只看
cap变没变:oldCap := cap(s); s = append(s, x); if cap(s) != oldCap { /* 扩容了 */ } - 别依赖扩容后容量“精确等于某值”,Go 只保证“足够大”,具体值是运行时实现细节
底层数组共享是把双刃剑
只要没扩容,append 返回的新切片和原切片共用同一底层数组——这省内存,但也埋雷。
- 常见错误现象:传一个切片进函数做
append并返回,调用方还拿着旧变量操作,结果发现数据被意外改了 - 安全做法:需要隔离时,显式复制:
newSlice := append([]int(nil), oldSlice...)(注意末尾...) - 千万别存
&s[i]这种地址——一次扩容后,该指针就成悬垂指针,后续解引用可能 panic 或读脏数据 - 调试技巧:
fmt.Printf("%p", &s[0])打印首元素地址,两次调用间变了,说明底层数组已迁移
nil 切片能 append,但混用容易翻车
var s []int 是合法的 nil 切片,append(s, 1) 会自动分配底层数组,行为完全安全——但问题出在“混合使用”上。
立即学习“go语言免费学习笔记(深入)”;
- 常见错误现象:函数参数允许
nil,内部却直接append(s, x);而调用方有时传make([]int, 0, 100),有时传nil,导致底层数组生命周期不可控 - 统一做法:函数入口先标准化:
if s == nil { s = make([]int, 0) },再后续append - 预分配有明确预期时,优先用
make([]int, 0, expectedCap),避免首次append分配小数组再反复扩容
最复杂的地方不在扩容公式,而在“共享”与“隔离”的边界模糊——你永远不知道哪个 append 会悄悄换掉底层数组,所以但凡涉及指针、map 存储、跨 goroutine 共享,就得提前掐断共享链。


















