Golang - 陣列、切片與映射的迭代(for‑range)
簡介
在 Go 語言中,陣列(array)、切片(slice) 與 映射(map) 是最常使用的集合型別。無論是處理資料清單、緩衝區,或是鍵值對的查詢,都離不開對這些容器的遍歷(iteration)。
Go 為了讓遍歷更簡潔、可讀,提供了 for‑range 這個語法結構。它能同時取得 索引(或鍵) 與 值,且自動處理底層的指標與長度檢查,讓開發者不必手動寫 for i := 0; i < len(x); i++ 那樣的樣板程式碼。
本篇文章將從 概念、實作範例、常見陷阱、最佳實踐 以及 實務應用 四個層面,完整說明 for‑range 在陣列、切片與映射上的使用方式,幫助初學者快速上手,也讓已有基礎的開發者能夠寫出更安全、效能更佳的程式。
核心概念
1. for‑range 的基本語法
for key, value := range collection {
// 在此使用 key 與 value
}
collection可以是陣列、切片、映射、字串或通道(channel)。key:對於陣列與切片是索引(int),對於映射是鍵的類型(如string、int)。value:對應的元素值。若只需要其中之一,可使用_佔位符忽略。
小技巧:若只需要值而不在意索引,寫成
for _, v := range xs { … };若只需要索引,寫成for i := range xs { … }。
2. 迭代陣列
陣列的長度在編譯期就已確定,for‑range 會依序遍歷每個元素,不會改變原始陣列。
package main
import "fmt"
func main() {
// 宣告一個長度為 5 的整數陣列
var nums = [5]int{10, 20, 30, 40, 50}
// 只取得值
for _, v := range nums {
fmt.Printf("值: %d\n", v)
}
// 同時取得索引與值
for i, v := range nums {
fmt.Printf("索引 %d => 值 %d\n", i, v)
}
}
注意:陣列是值類型(value type),若在迭代過程中把
v改成v = 0,不會影響nums本身。
3. 迭代切片
切片是基於陣列的抽象,長度與容量在執行時可變。for‑range 仍會返回 索引(相對於切片的起始位置)與 值。
package main
import "fmt"
func main() {
// 建立一個切片
fruits := []string{"蘋果", "香蕉", "櫻桃"}
// 只要值
for _, f := range fruits {
fmt.Println("水果:", f)
}
// 同時取得索引
for i, f := range fruits {
fmt.Printf("第 %d 個水果是 %s\n", i, f)
}
// 修改切片內的元素(直接改變底層陣列)
for i := range fruits {
fruits[i] = fruits[i] + " (好吃)"
}
fmt.Println("更新後:", fruits)
}
關鍵:
for i := range fruits只取得索引,不會產生值的副本,因此在迴圈內直接寫fruits[i] = …可以改變切片內容。
4. 迭代映射(map)
映射的遍歷順序是隨機的(不保證插入順序),每次執行程式可能得到不同的順序。for‑range 會同時返回 鍵 與 值。
package main
import "fmt"
func main() {
// 建立一個 map
scores := map[string]int{
"Alice": 85,
"Bob": 92,
"Carol": 78,
}
// 同時取得鍵和值
for name, score := range scores {
fmt.Printf("%s 的分數是 %d\n", name, score)
}
// 只需要鍵
for name := range scores {
fmt.Println("玩家:", name)
}
// 只需要值(使用 _ 忽略鍵)
for _, score := range scores {
fmt.Println("分數:", score)
}
}
提醒:因為遍歷順序不固定,若程式邏輯依賴順序(例如要產生報表),必須先把鍵抽出排序,再逐一取值。
5. for‑range 與指標、切片的結合
在某些情況下,我們想要 直接取得元素的指標,以免每次都產生值的副本。可以在迭代切片時使用指標:
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 4, 5}
// 取得每個元素的指標
for i := range nums {
p := &nums[i] // p 為 *int
*p = *p * 10 // 直接修改底層陣列
}
fmt.Println(nums) // [10 20 30 40 50]
}
最佳實踐:若僅需要讀取值,仍建議使用
value(非指標)以保持程式的可讀性與安全性;只有在大量寫入或需要共享同一個變數時,才考慮使用指標。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的寫法 |
|---|---|---|
| 迭代時誤改變迭代變數 | for _, v := range slice 取得的是 值的副本,若在迴圈內改 v,不會影響原 slice。 |
若要改變元素,使用索引:for i := range slice { slice[i] = … } |
| 閉包捕獲迭代變數 | 在 for i, v := range xs { go func(){ fmt.Println(i, v) }() } 中,i、v 會被所有 goroutine 共享,導致結果錯亂。 |
在迴圈內建立新變數:i, v := i, v; go func(){ … }() |
| map 的遍歷順序不固定 | 直接依序輸出 map 可能得到不同結果,影響測試或報表。 | 把鍵抽出切片,使用 sort.Strings(keys)(或相應類型)排序後再取值。 |
| 切片的容量擴增導致指標失效 | 在迭代過程中若對切片執行 append,可能觸發底層陣列重新分配,導致先前取得的指標失效。 |
避免在遍歷時同時 append 同一切片,或先把要加入的元素收集到另一個切片,迭代結束後一次 append。 |
忘記使用 _ 忽略不需要的返回值 |
只想要索引卻寫成 for i, _ := range xs,會產生不必要的變數。 |
正確寫法:for i := range xs 或 for _, v := range xs。 |
最佳實踐小結
- 盡量使用
for‑range取代傳統for i := 0; i < len(x); i++,提升可讀性。 - 只取需要的返回值,用
_省略其餘,減少記憶體開銷。 - 若要修改容器內容,使用索引或指標,避免直接改變
value副本。 - 在多執行緒(goroutine)環境,務必在迴圈內建立局部變數,以防止閉包捕獲同一個迭代變數。
- 對 map 需要有序輸出時,先排序鍵再遍歷。
實際應用場景
| 場景 | 為何使用 for‑range |
範例簡述 |
|---|---|---|
| 資料庫查詢結果轉換 | 查詢回傳的 []Row 需要逐筆處理並寫入另一個結構。 |
for _, r := range rows { dst = append(dst, convert(r)) } |
| 統計字串出現次數 | 使用 map[string]int 來計數,最後遍歷產生報表。 |
for word, cnt := range counter { fmt.Printf("%s: %d\n", word, cnt) } |
| 批次處理檔案 | 讀取目錄下的檔名切片,逐一開啟、處理。 | for _, name := range files { process(filepath.Join(dir, name)) } |
| 併發工作分配 | 把切片的工作項目分配給多個 goroutine,使用閉包捕獲索引時需特別處理。 | for i, job := range jobs { i, job := i, job; go worker(i, job) } |
| JSON 序列化/反序列化 | 把結構切片轉成 JSON 陣列,或把 JSON 陣列解析回切片。 | for _, item := range data { json.Marshal(item) } |
總結
for‑range是 Go 中遍歷 陣列、切片、映射 的首選語法,提供 索引/鍵 + 值 兩個返回值,讓程式碼更簡潔、易讀。- 迭代時要注意 值的副本 vs. 原始資料、閉包捕獲變數、以及 map 的隨機順序,這些都是常見的陷阱。
- 透過 索引、指標 或 局部變數 的正確使用,可安全地在迴圈內修改資料或啟動 goroutine。
- 在實務開發中,
for‑range幾乎無所不在:從資料清理、統計分析、批次處理到併發工作分配,都能發揮其簡化迭代的威力。
掌握了 for‑range 的正確用法與最佳實踐,你就能在 Go 程式開發中寫出更清晰、安全、且效能良好的程式碼。祝你在 Golang 的學習旅程中,迭代出更多精彩的作品!