append 的行為取決於 cap 夠不夠

同一個 append,會依底層 array 的 capacity 還夠不夠,走向兩種完全相反的結果。這是 slice 最大的認知負擔來源。

cap 夠 → 就地寫入,兩個 slice 仍共用同一塊 array:

arr := [3]int{1, 2, 3}
slice := arr[0:2]              // len=2 cap=3
newSlice := append(slice, 3)   // cap 夠,不建新 array
slice[0] = 999
// slice    → [999 2]
// newSlice → [999 2 3]   ← 一起被改

cap 不夠 → 配置更大的新 array、整份複製過去,兩者從此分家:

slice := []int{1, 2, 3}        // len=3 cap=3
newSlice := append(slice, 4)   // cap 不夠 → relocate
slice[0] = 999
// slice    → [999 2 3]
// newSlice → [1 2 3 4]   ← 沒被改到

因為結果無法預測,慣用寫法一律把結果指派回原變數

slice = append(slice, 4)

否則手上會同時有兩個變數,可能指向同一塊也可能指向不同塊 array,改了 A 卻沒改到 B。

每次擴容 = 一次配置 + 一次整份複製

array 的大小不能改,cap 滿了就得開一塊更大的、把舊值全部複製過去:

var s []int
for i := 0; i < 10; i++ {
    s = append(s, i)
    fmt.Println(len(s), cap(s))
}
// len=1 cap=1   ← 配置
// len=2 cap=2   ← 配置 + 複製 1 個
// len=3 cap=4   ← 配置 + 複製 2 個
// len=5 cap=8   ← 配置 + 複製 4 個
// len=9 cap=16  ← 配置 + 複製 8 個

append 10 次,觸發了 5 次配置與複製。 所以已知數量時要先開好容量:

// 方法一:len=0 先給 cap,搭配 append
contacts := make([]Contact, 0, len(src))
contacts = append(contacts, c)
 
// 方法二:直接給 len,用 index 指派
contacts := make([]Contact, len(src))
contacts[i] = c

成長倍率的實際數字不要背,見 Go 的 slice 擴容倍率在大 slice 時還是 2 倍嗎

相關:切割出的 slice 預設 cap 吃到底,append 會寫進鄰居的地盤copy 是唯一真的複製 slice 資料的方式