本文 AI 產出,尚未審核

Golang – 陣列、切片與映射

單元:映射(maps)的宣告與操作


簡介

在 Go 語言中,映射(map) 是一種內建的雜湊資料結構,提供了「鍵 → 值」的對應關係。相較於陣列或切片只能以整數索引存取資料,map 允許使用任何可比較(comparable)的型別作為鍵,讓我們能以更自然的方式表達「字典」或「關聯陣列」的概念。

掌握 map 的正確宣告與操作,不僅能簡化程式邏輯,還能提升效能與可讀性。無論是 設定使用者設定檔統計字頻、或是 快取資料,map 都是不可或缺的工具。本篇文章將從基礎宣告開始,逐步帶你了解常見的 CRUD(Create、Read、Update、Delete)操作、迭代方式、以及在實務開發中需要注意的陷阱與最佳實踐。


核心概念

1. Map 的基本宣告

Go 的 map 需要在宣告時指定 鍵的型別值的型別,語法如下:

var m map[keyType]valueType

然而,僅使用 var 會得到一個 nil map,此時若直接寫入會觸發 panic。常見的建立方式有兩種:

  1. 使用 make
  2. 使用字面值(map literal)

範例 1:使用 make 建立空的 map

package main

import "fmt"

func main() {
    // 建立一個以字串為鍵、整數為值的 map
    scores := make(map[string]int)

    // 直接寫入鍵值對
    scores["Alice"] = 85
    scores["Bob"] = 92

    fmt.Println(scores) // map[Alice:85 Bob:92]
}

重點make 會在底層分配記憶體,使 map 成為可寫入的狀態。

範例 2:使用字面值一次建立多筆資料

package main

import "fmt"

func main() {
    // 直接以字面值建立 map
    countryCode := map[string]string{
        "TW": "Taiwan",
        "JP": "Japan",
        "US": "United States",
    }

    fmt.Println(countryCode["JP"]) // Japan
}

2. 讀取與檢查鍵是否存在

直接使用 m[key] 會回傳對應的值,若鍵不存在則回傳該值型別的 零值。若需要同時判斷鍵是否真的存在,必須使用「雙賦值」語法:

value, ok := m[key]
  • value:鍵對應的值(若不存在則為零值)
  • ok:布林值,鍵存在時為 true,否則 false

範例 3:安全讀取與判斷

package main

import "fmt"

func main() {
    ages := map[string]int{
        "Tom":  28,
        "Lily": 31,
    }

    // 嘗試讀取不存在的鍵
    if age, ok := ages["John"]; ok {
        fmt.Printf("John is %d years old.\n", age)
    } else {
        fmt.Println("John 的年齡資料不存在。")
    }

    // 正常讀取
    if age, ok := ages["Tom"]; ok {
        fmt.Printf("Tom is %d years old.\n", age)
    }
}

3. 更新與刪除

  • 更新:直接指派新值即可,若鍵不存在則會自動新增。
  • 刪除:使用內建函式 delete(map, key),若鍵不存在則不會產生錯誤。

範例 4:更新與刪除

package main

import "fmt"

func main() {
    inventory := map[string]int{
        "apple":  5,
        "banana": 3,
    }

    // 更新:把 apple 數量加 2
    inventory["apple"] += 2 // 現在是 7

    // 新增:加入 orange
    inventory["orange"] = 4

    // 刪除 banana
    delete(inventory, "banana")

    fmt.Println(inventory) // map[apple:7 orange:4]
}

4. 迭代 Map

使用 for range 可以遍歷 map,返回 。需要注意的是,map 的遍歷順序是 隨機且不保證穩定,若需要有序輸出,必須自行排序鍵或值。

範例 5:遍歷與排序

package main

import (
    "fmt"
    "sort"
)

func main() {
    scores := map[string]int{
        "Alice": 88,
        "Bob":   95,
        "Cathy": 73,
    }

    // 直接遍歷(順序不固定)
    fmt.Println("未排序的遍歷:")
    for name, score := range scores {
        fmt.Printf("%s => %d\n", name, score)
    }

    // 若要依名稱字母排序
    fmt.Println("\n依名稱排序後的遍歷:")
    keys := make([]string, 0, len(scores))
    for k := range scores {
        keys = append(keys, k)
    }
    sort.Strings(keys) // 依字母排序

    for _, k := range keys {
        fmt.Printf("%s => %d\n", k, scores[k])
    }
}

5. Map 的零值與初始化

  • nil map:未使用 make 或字面值建立的 map 為 nil,只能讀取(會得到零值)或判斷是否為 nil,寫入會 panic。
  • 空 map:使用 make 建立的 map 雖然長度為 0,但已可安全寫入。

範例 6:nil 與空 map 的差異

package main

import "fmt"

func main() {
    var nilMap map[string]int // nil map
    emptyMap := make(map[string]int) // 空 map

    fmt.Println("nilMap == nil :", nilMap == nil)   // true
    fmt.Println("len(nilMap) :", len(nilMap))      // 0

    // 寫入 nil map 會 panic
    // nilMap["a"] = 1 // <-- 會觸發 runtime panic

    // 寫入空 map是安全的
    emptyMap["a"] = 1
    fmt.Println("emptyMap :", emptyMap) // map[a:1]
}

常見陷阱與最佳實踐

陷阱 說明 建議的做法
直接寫入 nil map 未初始化的 map 只能讀取,寫入會 panic。 使用 make字面值 初始化;若不確定是否為 nil,先檢查 if m == nil { m = make(map[...]) }
鍵的可比較性 只能使用可比較的型別(如 slice、map、function)作為鍵會編譯錯誤。 使用 基本型別指標、或 自訂結構(只要所有欄位皆可比較)
遍歷順序不固定 依賴遍歷順序的程式在不同執行時可能得到不同結果。 若需順序,先收集鍵 再排序,或使用 slice 搭配 map
共享底層結構 把 map 作為函式參數傳遞時,實際上是傳遞 引用,函式內的變更會影響外部。 若不想被修改,傳入副本newMap := make(map[...]); for k,v := range src { newMap[k]=v }
容量預估 預設容量為 0,頻繁擴容會影響效能。 使用 make(map[...], initialCap) 預估容量,減少重新分配次數

最佳實踐小結

  1. 初始化即使用:除非有特別需求,盡量以 make 或字面值直接建立 map。
  2. 雙賦值檢查:讀取前使用 value, ok := m[key],避免誤把零值當成有效資料。
  3. 避免在高併發環境直接寫入同一 map:若多個 goroutine 同時寫入,需要使用 sync.RWMutexsync.Map(Go 1.9+)保護。
  4. 適度預估容量:對大資料量的 map,使用 make(map[...], cap) 可提升效能。
  5. 保持鍵的唯一性:若鍵是自訂結構,確保所有欄位的值能唯一辨識資料,避免意外覆寫。

實際應用場景

場景 為何適合使用 map 範例簡述
字頻統計 需要快速查詢與累加字元出現次數 讀取文字後 freq[ch]++
快取 (Cache) 以唯一鍵(如 URL)快速取得緩存資料 cache[url] = responseBody
設定檔解析 以鍵值對形式儲存設定項目,支援動態查詢 讀取 JSON 後 config["port"]
關聯式資料 把 ID 映射到完整結構,避免重複搜尋 users[id] = User{...}
圖形演算法 用鄰接表表示圖的連接關係 adjList[node] = []int{...}

小技巧:在需要頻繁讀寫且資料量不大時,直接使用 map;若資料量極大或需要持久化,考慮結合 sync.MapLRU Cache 或外部資料庫。


總結

  • Map 是 Go 中最彈性的鍵值資料結構,支援任意可比較的鍵型別。
  • 正確的 初始化make 或字面值)是避免 panic 的第一步。
  • 使用 雙賦值 讀取可以同時取得值與鍵是否存在的資訊,提升程式安全性。
  • 迭代順序不保證,若需有序輸出,必須自行排序鍵或值。
  • 高併發大型資料 場景下,適時使用 sync.RWMutexsync.Map預估容量,可顯著提升效能與穩定性。

掌握了 map 的宣告與操作,你就能在日常開發中快速構建字典、快取、設定管理等多種功能,為 Go 程式的可讀性與效能奠定堅實基礎。祝你在 Golang 的世界裡玩得開心,寫出更乾淨、更高效的程式碼!