Go 的指標讓函式可以操作原本的資料,而不是資料複本。這很有效率,也很危險:當多個地方共享同一份資料時,你需要明確決定誰可以修改,誰只能讀取。

本章目標

學完本章後,你將能夠:

  1. 理解值傳遞與指標傳遞的差異
  2. 判斷何時使用 pointer
  3. 理解 slice、map 本身已經帶有共享底層資料的特性
  4. 用 copy 保護資料邊界

【觀察】Go 預設是值傳遞

值傳遞的核心規則是:函式收到的是參數值的複本,修改複本不會改到呼叫端原值。以下範例中,Rename 修改的是複本:

 1type User struct {
 2    Name string
 3}
 4
 5func Rename(u User) {
 6    u.Name = "Bob"
 7}
 8
 9func main() {
10    user := User{Name: "Alice"}
11    Rename(user)
12    fmt.Println(user.Name) // Alice
13}

Rename 修改的是複本,不是 main 裡的 user

指標傳遞的核心規則是:函式收到原值位址,因此可以修改呼叫端原值。如果想修改原本的值,就要傳指標:

1func Rename(u *User) {
2    u.Name = "Bob"
3}
4
5func main() {
6    user := User{Name: "Alice"}
7    Rename(&user)
8    fmt.Println(user.Name) // Bob
9}

&user 取得位址,*User 表示指向 User 的指標。

【判讀】pointer 表示共享修改權

pointer 的核心語意是共享修改權,不只是效能工具。它表示被呼叫者可能看到或修改原本那份資料。

適合使用 pointer 的情境:

情境原因
方法需要修改 receiver例如 Counter.Inc()
struct 很大,複製成本高避免每次呼叫都複製大量資料
需要表示 optional objectnil 可表示不存在
多個方法共享同一份狀態例如 repository、server、cache

不適合濫用 pointer 的情境:

  • 小型不可變資料,例如 time.Time 常直接值傳遞
  • 只是為了「看起來像物件導向」
  • 不希望呼叫者能修改內部資料

【策略】slice 和 map 要特別小心

slice 和 map 的核心風險是:即使參數不是 pointer,也會共享底層資料。

slice 共享底層陣列

slice 參數會複製 slice header,但 header 指向同一個底層 array;因此函式內修改元素,外面會看見。

1func Modify(items []string) {
2    items[0] = "changed"
3}
4
5func main() {
6    names := []string{"Alice", "Bob"}
7    Modify(names)
8    fmt.Println(names[0]) // changed
9}

map 本身就是 reference-like

map 傳入函式後,函式可以修改同一份 map。這是很多共享狀態 bug 的來源。

1func Modify(m map[string]int) {
2    m["count"] = 10
3}
4
5func main() {
6    values := map[string]int{"count": 1}
7    Modify(values)
8    fmt.Println(values["count"]) // 10
9}

【執行】回傳資料時建立 copy 邊界

copy 邊界的核心規則是:不希望外部修改內部狀態時,不要直接回傳內部 map、slice 或 pointer。假設 UserRepository 內部保存一組使用者:

1type User struct {
2    ID   string
3    Name string
4}
5
6type UserRepository struct {
7    users map[string]User
8}

直接回傳 map 會把內部狀態暴露給呼叫者:

1func (r *UserRepository) Users() map[string]User {
2    return r.users
3}

呼叫者就可以繞過 UserRepository 修改內部資料:

1users := repo.Users()
2users["1"] = User{ID: "1", Name: "Changed"}

安全做法是回傳複製:

1func (r *UserRepository) Users() map[string]User {
2    result := make(map[string]User, len(r.users))
3    for id, user := range r.users {
4        result[id] = user
5    }
6    return result
7}

回傳 slice 時也要複製 slice:

1func (r *UserRepository) ListUsers() []User {
2    result := make([]User, 0, len(r.users))
3    for _, user := range r.users {
4        result = append(result, user)
5    }
6    return result
7}

這樣呼叫者可以自由排序、append 或修改回傳資料,不會影響 repository 內部狀態。

深層複製與淺層複製

深層複製的核心規則是:struct 裡面若含 slice、map 或 pointer,只複製 struct 本身仍會共享內部資料。以下 Profile 包含 slice:

1type Profile struct {
2    Name string
3    Tags []string
4}

淺層複製:

1copyProfile := profile

copyProfile.Tagsprofile.Tags 仍然指向同一個底層 array。若要保護邊界,需要複製 slice:

1func CloneProfile(p Profile) Profile {
2    p.Tags = append([]string(nil), p.Tags...)
3    return p
4}

這種 copy 邊界在共享狀態、快取、API response、測試資料中都很重要。