Go Slice 隐式共享陷阱 & 内存对齐

Go Slice 隐式共享陷阱 & 内存对齐

reddit 上看到的有意思的东西…

问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
a1 := make([]int, 0, 0)
a1 = append(a1, []int{1, 2, 3, 4, 5}...)
a2 := append(a1, 6)
a3 := append(a1, 7)

fmt.Println(a1, a2, a3)
// output: [1 2 3 4 5] [1 2 3 4 5 7] [1 2 3 4 5 7]
}

a2 本应是 [1 2 3 4 5 6],结果却是 7

根本原因:底层数组共享

Go 的 slice 是 { 指针, len, cap } 三元组。

append 在 cap 够用时不分配新数组,直接写入原底层数组。a2 := append(a1, 6)a3 := append(a1, 7) 都基于同一个 a1(len=5),两次都写入 index 5 的同一位置,后者覆盖前者。

1
2
3
4
5
6
7
8
9
a1 → [1][2][3][4][5][ ]   len=5, cap=6,index 5 空着

a2 = append(a1, 6) → 写入 index 5 = 6
[1][2][3][4][5][6]

a3 = append(a1, 7) → 再次写入 index 5 = 7(覆盖!)
[1][2][3][4][5][7]

a2 和 a3 共享同一底层数组,所以 a2[5] 也变成了 7

为什么 cap 是 6 而不是 5?

内存对齐

CPU 读内存按固定宽度的”块”(通常 8 字节)读取,数据必须存放在对齐地址上:

1
2
int64 (8字节) → 必须放在 8 的倍数地址
int32 (4字节) → 必须放在 4 的倍数地址

Size Class

Go 的内存分配器(基于 tcmalloc)有固定的 size class,不会分配任意大小的内存:

1
需要 5 × 8 = 40 字节 → 最近的 size class 是 48 字节 → 能放 6 个 int64

所以 cap 变成 6,多出的一格给了 a2/a3 可直接写入的空位,引发覆盖问题。

解决方案

追加前用完整切片表达式限制 cap,强制 append 分配新数组:

1
2
3
4
a1 = a1[:len(a1):len(a1)]

a2 := append(a1, 6) // 新数组
a3 := append(a1, 7) // 新数组,互不影响

总结

对齐凑整导致 cap 比预期多,这个”意外”的空位是 slice 共享陷阱的导火索。