本文 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。常見的建立方式有兩種:
- 使用
make - 使用字面值(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) 預估容量,減少重新分配次數 |
最佳實踐小結
- 初始化即使用:除非有特別需求,盡量以
make或字面值直接建立 map。 - 雙賦值檢查:讀取前使用
value, ok := m[key],避免誤把零值當成有效資料。 - 避免在高併發環境直接寫入同一 map:若多個 goroutine 同時寫入,需要使用 sync.RWMutex 或 sync.Map(Go 1.9+)保護。
- 適度預估容量:對大資料量的 map,使用
make(map[...], cap)可提升效能。 - 保持鍵的唯一性:若鍵是自訂結構,確保所有欄位的值能唯一辨識資料,避免意外覆寫。
實際應用場景
| 場景 | 為何適合使用 map | 範例簡述 |
|---|---|---|
| 字頻統計 | 需要快速查詢與累加字元出現次數 | 讀取文字後 freq[ch]++ |
| 快取 (Cache) | 以唯一鍵(如 URL)快速取得緩存資料 | cache[url] = responseBody |
| 設定檔解析 | 以鍵值對形式儲存設定項目,支援動態查詢 | 讀取 JSON 後 config["port"] |
| 關聯式資料 | 把 ID 映射到完整結構,避免重複搜尋 | users[id] = User{...} |
| 圖形演算法 | 用鄰接表表示圖的連接關係 | adjList[node] = []int{...} |
小技巧:在需要頻繁讀寫且資料量不大時,直接使用 map;若資料量極大或需要持久化,考慮結合 sync.Map、LRU Cache 或外部資料庫。
總結
- Map 是 Go 中最彈性的鍵值資料結構,支援任意可比較的鍵型別。
- 正確的 初始化(
make或字面值)是避免 panic 的第一步。 - 使用 雙賦值 讀取可以同時取得值與鍵是否存在的資訊,提升程式安全性。
- 迭代順序不保證,若需有序輸出,必須自行排序鍵或值。
- 在 高併發 或 大型資料 場景下,適時使用 sync.RWMutex、sync.Map 或 預估容量,可顯著提升效能與穩定性。
掌握了 map 的宣告與操作,你就能在日常開發中快速構建字典、快取、設定管理等多種功能,為 Go 程式的可讀性與效能奠定堅實基礎。祝你在 Golang 的世界裡玩得開心,寫出更乾淨、更高效的程式碼!