slice 和 map 是 Go 最常用的集合型別。slice 表達有順序的資料列表,map 表達 key-value 查詢表。理解它們的行為,是寫出可靠 Go 程式的基本功。

本章目標

學完本章後,你將能夠:

  1. 建立與操作 slice
  2. 理解 slice 的長度、容量與 append
  3. 建立與操作 map
  4. 判斷何時使用 slice,何時使用 map
  5. 避免 nil slice、nil map 與共享底層資料的常見問題

【觀察】slice 表達有順序的資料

slice 是 Go 中用來表示有順序元素列表的集合型別。以下範例建立一個 []string,並依照索引順序走訪:

1names := []string{"Alice", "Bob", "Carol"}
2
3for i, name := range names {
4    fmt.Println(i, name)
5}

slice 的常見操作是讀取元素、取得長度、用 append 增加元素:

1names = append(names, "Dave")
2first := names[0]
3count := len(names)

append 的核心規則是:它會回傳 append 後的 slice,呼叫端必須接回結果。len(names) 取得元素數量;append 可能重用原底層 array,也可能配置新底層 array:

1names = append(names, "Eve")

【判讀】slice 是對底層 array 的視窗

slice 的核心模型是「指向底層 array 的視窗」,不是 array 本身。它比較像一個描述底層 array 區段的 header,包含:

1pointer -> 底層 array
2len     -> 目前看得到幾個元素
3cap     -> 從起點到底層 array 結尾還有多少容量

長度與容量分別描述「目前元素數」與「不重新配置時還能擴張多少」。以下範例可以觀察 lencap 的變化:

1items := make([]int, 0, 3)
2fmt.Println(len(items), cap(items)) // 0 3
3
4items = append(items, 10)
5fmt.Println(len(items), cap(items)) // 1 3

append 超過容量時,Go 可能會配置新的底層 array:

1items = append(items, 20, 30, 40)
2fmt.Println(len(items), cap(items))

這就是 append 必須接回原變數的原因:append 後的 slice 可能已經指向新的底層資料。

【策略】用 slice 保存順序,用 map 做查詢

選擇集合型別的核心規則是:在意順序用 slice,需要 key-value 查詢用 map。如果你在意資料順序,用 slice:

1tasks := []string{"read", "write", "test"}

如果你要用 key 快速查資料,用 map:

1scores := map[string]int{
2    "Alice": 90,
3    "Bob":   85,
4}

讀取 map:

1score, ok := scores["Alice"]
2if ok {
3    fmt.Println(score)
4}

map 讀取的核心規則是:需要分辨「不存在」和「零值」時,必須使用 value, ok。key 不存在時,map 會回傳 value type 的零值:

1score := scores["Unknown"] // 0

如果不檢查 ok,你無法分辨「不存在」和「存在但分數是 0」。

【執行】nil slice 與 nil map 的差異

nil slice 和 nil map 的核心差異是:nil slice 可以 append,nil map 不能寫入。nil slice 可以 append:

1var names []string
2names = append(names, "Alice")

nil map 不能直接寫入:

1var scores map[string]int
2scores["Alice"] = 90 // panic

map 寫入前必須先初始化:

1scores := make(map[string]int)
2scores["Alice"] = 90

或用 literal:

1scores := map[string]int{
2    "Alice": 90,
3}

slice 和 map 的常見組合

用 slice 保存輸出順序

map 的迭代順序不保證穩定;如果輸出順序重要,必須額外用 slice 保存或排序 key:

1for name, score := range scores {
2    fmt.Println(name, score)
3}

先整理 key:

1names := make([]string, 0, len(scores))
2for name := range scores {
3    names = append(names, name)
4}
5sort.Strings(names)
6
7for _, name := range names {
8    fmt.Println(name, scores[name])
9}

用 map 當 set

Go 沒有內建 set;需要集合語義時,常用 map[string]struct{} 表示「某個 key 是否存在」:

1seen := make(map[string]struct{})
2seen["Alice"] = struct{}{}
3
4if _, ok := seen["Alice"]; ok {
5    fmt.Println("already seen")
6}

如果想更直觀,也可以用 map[string]bool

1seen := map[string]bool{
2    "Alice": true,
3}

設計檢查

檢查一:接住 append 回傳值

1append(names, "Alice") // 編譯錯誤:append 結果未使用

正確做法:

1names = append(names, "Alice")

檢查二:寫入前初始化 map

1var m map[string]int
2m["x"] = 1 // panic

正確做法:

1m := make(map[string]int)
2m["x"] = 1

檢查三:需要順序時先排序 key

map 的順序不能拿來做穩定輸出、測試 snapshot 或 UI 排序。需要順序就額外維護 slice。