for range遍历slice时先复制快照(array指针、len、cap),循环次数和元素值均由快照决定,append扩容不影响遍历行为。
for range 遍历时的 slice 快照机制
go 的 for range 遍历 slice 时,不是实时读取原 slice,而是在循环开始前复制一份快照——包含当时的 array 指针、len 和 cap。这三个值一旦确定,整个循环的迭代次数、索引范围、元素值就完全固定了。
这意味着:你在循环体里对原 slice 做任何 append、s = s[1:] 或甚至重新赋值,都不会让 range 多跑一次或少跑一次。它只认快照里的 len,不关心后续扩容是否导致底层数组迁移。
常见错误现象:s := []int{1,2,3}; for _, v := range s { s = append(s, v) } → 最终 s 是 [1 2 3 1 2 3],不会无限循环;但若误以为 range 能“看到”新追加的元素,逻辑就会出错。
循环内 append 后取地址(&v)的陷阱
在 for range 中,变量 v 是每次迭代的**副本**,不是原 slice 元素的引用。即使你对 v 取地址(&v),得到的也是该副本的地址,和原底层数组无关。
更危险的是:如果循环中 append 触发扩容,底层数组迁移,而你之前保存的某个 &s[i] 地址可能已失效(指向旧内存),但 Go 不会报错,只会读到旧值或未定义内容。
使用场景:想边遍历边收集指针(如 []*T)时,必须显式用索引取地址:
s := []int{1,2,3}
ptrs := make([]*int, 0, len(s))
for i := range s {
ptrs = append(ptrs, &s[i]) // ✅ 正确:每次取当前底层数组真实地址
}
// 而不是:
for _, v := range s {
ptrs = append(ptrs, &v) // ❌ 错误:所有 &v 指向同一个栈变量地址
}
扩容后原 slice 与快照的“断连”现象
当 append 导致扩容,原 slice 的 array 字段会被更新为新地址;但 for range 的快照仍持有旧 array 指针。这本身安全(因为快照只读初始长度内的旧数据),但容易掩盖问题:
- 若你在循环中修改
s[i](用索引),且i在快照len范围内,改的是旧底层数组 —— 但后续append后的新元素不在其中,逻辑割裂 - 若函数参数是
[]T,调用方传入nil,你在循环内append后扩容出新数组,调用方仍看到nil,引用完全丢失 - 并发场景下,另一 goroutine 对同一 slice 执行
append,本 goroutine 的range仍按快照执行,看似“线程安全”,实则读不到最新状态
为什么预分配容量能缓解引用混乱
预分配(make([]T, 0, N))不改变 for range 的语义,但它减少了扩容概率,从而降低“断连”发生频率:
若 cap 足够,append 不触发 growslice,原 slice 的 array 指针全程不变,快照与原 slice 始终共享同一底层数组 —— 此时用 &s[i] 是稳定可靠的。
容易被忽略的点:即使预分配了容量,只要循环中执行了 s = append(s, x) 且 len(s) == cap(s),仍会扩容;真正安全的做法是:要么避免在 range 中修改原 slice,要么改用传统 for i := 0; i 并确保 <code>i 不越界。


















