2.5 指標與資料複製邊界
Go 的指標讓函式可以操作原本的資料,而不是資料複本。這很有效率,也很危險:當多個地方共享同一份資料時,你需要明確決定誰可以修改,誰只能讀取。
本章目標
學完本章後,你將能夠:
- 理解值傳遞與指標傳遞的差異
- 判斷何時使用 pointer
- 理解 slice、map 本身已經帶有共享底層資料的特性
- 用 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 object | nil 可表示不存在 |
| 多個方法共享同一份狀態 | 例如 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 := profilecopyProfile.Tags 和 profile.Tags 仍然指向同一個底層 array。若要保護邊界,需要複製 slice:
1func CloneProfile(p Profile) Profile {
2 p.Tags = append([]string(nil), p.Tags...)
3 return p
4}這種 copy 邊界在共享狀態、快取、API response、測試資料中都很重要。