append后底层数组地址是否变化,只取决于len(s) == cap(s):成立则必扩容、地址大概率变;不成立则复用原数组、&s[0]不变。

append 后底层数组地址是否变化,只看 len(s) == cap(s)
地址变或不变,不取决于元素个数、类型、Go 版本,甚至不取决于“是不是第一次 append”,只看当前切片的长度是否已满容量。成立则必扩容,地址大概率变;不成立则复用原底层数组,&s[0] 不变。
常见误判:以为 make([]int, 3) 和 make([]int, 3, 3) 一样——其实完全等价,都是 len==cap==3,第一次 append 就触发扩容。
-
s := []int{1,2,3}→ len=3, cap=3 →append(s, 4)必扩容 -
s := make([]int, 3, 5)→ len=3, cap=5 →append(s, 4)不扩容,&s[0]不变 -
s := make([]int, 0, 100)→ len=0, cap=100 → 连续append100 次都不扩容
为什么 fmt.Printf("%p", s) 不是判断地址变化的可靠方式
fmt.Printf("%p", s) 打印的是 s.array 字段值,也就是底层数组首地址,这本身是对的;但问题在于,它容易和“切片变量自身的栈地址”混淆。有人看到两次打印结果相同,就误以为没扩容——其实只是碰巧新旧数组首地址数值一样(极少见),或根本没注意是否真发生了扩容。
更直接的方式是用 unsafe.Pointer(&s[0]) 对比,或者观察 cap 变化:
- 扩容前
cap == 5,扩容后cap == 8→ 地址变了 - 扩容前后
cap不变,且len < cap→ 地址没变 - 不要依赖
fmt.Printf("%p", &s)——那是切片头结构体的栈地址,永远不变
不同元素类型或大小会影响扩容后的 cap 值
Go 的 growslice 实现会按元素大小做对齐优化,所以 cap 增长不是纯数学翻倍。同一长度下,[]int 和 []uint8 的扩容后 cap 可能不同。
例如:
s1 := make([]int, 8, 10)<br>s1 = append(s1, 1, 2, 3) // len=11 → cap 通常变成 20
s2 := make([]uint8, 8, 10)<br>s2 = append(s2, 1, 2, 3) // len=11 → cap 可能变成 24(因对齐到 64 字节边界)
这意味着:即使你算准了“容量够用”,若类型变更或 Go 版本升级,cap 计算逻辑可能微调,唯一稳态解仍是预分配足够 cap。
函数传参和变量赋值时,append 导致的地址漂移最易被忽略
这是线上 bug 高发区:传进函数的切片在内部 append 后扩容,但没把返回值赋回给原变量,调用方仍持旧底层数组指针。
-
s := []int{1}; t := s; s = append(s, 2)→t和s底层数组已分离 -
func f(x []int) { x = append(x, 99) }→ 外部s不受影响,除非写成s = f(s) - CGO 场景下若把
&s[0]传给 C 函数,之后又无意识append,C 端访问的就是已释放内存
真正需要地址稳定的地方(如 mmap、cgo、unsafe.Slice),不能靠“运气不扩容”,必须初始化时用 make([]T, 0, N) 预留足量 cap。任何一次扩容都意味着旧地址作废。


















