Go 切片:指针悖论——为何你的 Append 消失(了解切片修改何时持久,何时消失)
发布: (2025年12月14日 GMT+8 03:09)
4 min read
原文: Dev.to
Source: Dev.to
func main() {
users := []*User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
}
updateUsers(users)
fmt.Println("After update:")
for _, u := range users {
fmt.Printf("%s: %d\n", u.Name, u.Age)
}
// Output:
// Alice: 40 - changed
// Bob: 30
// Charlie: ? - missing
}
func updateUsers(users []*User) {
// This change persists
users[0].Age = 40
// This change disappears
users = append(users, &User{Name: "Charlie", Age: 35})
}
为什么第一个修改会保留下来,而追加的元素会消失?
切片头部
Go 切片 不是 数组。内部由三个字段组成的头部:
// Conceptual slice header
type sliceHeader struct {
pointer *array // points to the underlying array
length int // number of elements in the slice
capacity int // size of the underlying array
}
当切片作为参数传递给函数时,头部会被复制;底层数组 不会 被复制。
[]*User 的两层间接引用
users (slice header)
│
├──→ [ptr1, ptr2] (array of pointers)
│ │ │
│ │ └──→ Bob struct
│ │
│ └──→ Alice struct
│
└── length: 2, capacity: 2
调用者和被调函数共享同一个指向底层数组的指针,所以修改指针指向的结构体(例如 users[0].Age = 40)会更新原始数据。
append 时会发生什么
当现有容量不足时,append 可能会分配一个 新数组。随后它会创建一个 新切片头部,指向这个新数组。由于头部是按值传递的,只有被调函数看到这个新头部;调用者仍然持有原来的头部。
Before call (in main):
users → [ptr1, ptr2] (len=2, cap=2)
Inside updateUsers after append:
users → [ptr1, ptr2, ptr3] (len=3, cap=4) // new header, possibly new array
After return:
main.users is unchanged → still length 2
ptr3 (Charlie) becomes unreachable → eligible for GC
让 append 对调用者可见的方式
1. 返回新的切片
func updateUsers(users []*User) []*User {
users[0].Age = 40
return append(users, &User{Name: "Charlie", Age: 35})
}
func main() {
users := []*User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
}
users = updateUsers(users)
// users now contains Charlie
}
2. 传入切片的指针
func updateUsers(users *[]*User) {
(*users)[0].Age = 40
*users = append(*users, &User{Name: "Charlie", Age: 35})
}
func main() {
users := []*User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
}
updateUsers(&users)
// users now contains Charlie
}
3. 使用指针接收者的方法
type UserList []*User
func (ul *UserList) AddUser(name string, age int) {
*ul = append(*ul, &User{Name: name, Age: age})
}
func main() {
users := UserList{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
}
users.AddUser("Charlie", 35)
// users now contains Charlie
}
关键要点
- 通过切片内部指针对结构体字段的 更新 会影响原始数据,因为底层数组是共享的。
- 对切片本身的 修改(改变长度或容量)除非返回新的头部或传入切片指针,否则 不会 传播到调用者。
- 可以把切片看作一个 值,它包含指针、长度和容量;指针部分是共享的,头部本身则不是。