2.2 slice 與 map
slice 和 map 是 Go 最常用的集合型別。slice 表達有順序的資料列表,map 表達 key-value 查詢表。理解它們的行為,是寫出可靠 Go 程式的基本功。
本章目標
學完本章後,你將能夠:
- 建立與操作 slice
- 理解 slice 的長度、容量與 append
- 建立與操作 map
- 判斷何時使用 slice,何時使用 map
- 避免 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 結尾還有多少容量長度與容量分別描述「目前元素數」與「不重新配置時還能擴張多少」。以下範例可以觀察 len 和 cap 的變化:
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 3append 超過容量時,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 // panicmap 寫入前必須先初始化:
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。