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
}

关键要点

  • 通过切片内部指针对结构体字段的 更新 会影响原始数据,因为底层数组是共享的。
  • 对切片本身的 修改(改变长度或容量)除非返回新的头部或传入切片指针,否则 不会 传播到调用者。
  • 可以把切片看作一个 ,它包含指针、长度和容量;指针部分是共享的,头部本身则不是。
Back to Blog

相关文章

阅读更多 »

使用 pprof 进行 Go 性能分析

什么是 pprof?pprof 是 Go 的内置分析工具,允许您收集和分析应用程序的运行时数据,例如 CPU 使用率、内存分配……

Go的秘密生活:接口

The Power of Implicit Contracts 星期二的早晨雾气弥漫。 伊桑手提咖啡和一小盒biscotti,走下去往档案室。 埃莉诺抬起头。 “...