Skip to main content

Command Palette

Search for a command to run...

淺談 Go Iterator

Published
10 min readView as Markdown
淺談 Go Iterator

Go 1.23 引入了原生的 Iterator 支援,這是 Go 語言在函數式程式設計道路上的重要里程碑。這篇將分享 Go Iterator 的設計理念、使用方法和實踐。

什麼是 Iterator?

Iterator(迭代器)是一個**函數**,它將序列中的連續元素傳遞給 callback 函數(通常命名為 yield)。當序列結束或 yield 返回 false 時,函數會停止迭代。

在 Go 1.23 中,Iterator 是語言的原生特性,介面定義在 iter 套件中。

Iterator 的核心設計

類型定義

Go 定義了兩種主要的 Iterator 類型:

type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

yield 函數的語意設計

yield 函數的核心語意是:"產出並詢問是否繼續"

// yield 函數返回值的意義:
// - true: 繼續迭代下一個元素
// - false: 停止迭代
func myIterator() iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 1; i <= 5; i++ {
            if !yield(i) { // 產出值並詢問是否繼續
                return // 尊重消費者的決定
            }
        }
    }
}

協作式控制流

Iterator 的設計體現了生產者和消費者之間的協作關係:

生產者視角的語意

func fibonacci() iter.Seq[int] {
    return func(yield func(int) bool) {
        a, b := 0, 1
        for {
            // 語意:"我計算出了一個值,你要嗎?"
            if !yield(a) {
                // 語意:"好的,你不要了,我停止生產"
                return
            }
            // 語意:"你還要,我繼續計算下一個"
            a, b = b, a+b
        }
    }
}

消費者視角的語意

// range 迴圈的隱含語意
for value := range fibonacci() {
    fmt.Println(value)
    if value > 100 {
        // 隱含語意:"我不需要更多了"
        break // 這會讓 yield 返回 false
    }
    // 隱含語意:"我處理完了,請給我下一個"
}

基本使用方法

1. 簡單的數值範圍迭代器

package main

import (
    "fmt"
    "iter"
)

// 整數範圍迭代器
func rangeSeq(start, end int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := start; i <= end; i++ {
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    // 使用 range 語法遍歷
    for num := range rangeSeq(1, 5) {
        fmt.Println(num) // 輸出: 1, 2, 3, 4, 5
    }
}

實際用途:

  • 測試資料生成:快速生成測試用的數字序列

  • 分頁處理:生成頁碼序列

2. Key-Value Pair 迭代器

// Map 迭代器
func mapSeq[K comparable, V any](m map[K]V) iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        for k, v := range m {
            if !yield(k, v) {
                return
            }
        }
    }
}

// 使用
data := map[string]int{"apple": 5, "banana": 3, "cherry": 8}
for key, value := range mapSeq(data) {
    fmt.Printf("%s: %d\n", key, value)
}

實際用途:

  • 配置處理:遍歷配置項目

  • 資料轉換:將 map 轉換為其他格式

  • 條件過濾:在遍歷過程中進行篩選

3. 集合類型的 Iterator

type Set[V comparable] struct {
    items map[V]struct{}
}

// 按照命名慣例,集合的迭代器方法命名為 All
func (s *Set[V]) All() iter.Seq[V] {
    return func(yield func(V) bool) {
        for item := range s.items {
            if !yield(item) {
                return
            }
        }
    }
}

// 使用
set := &Set[string]{items: map[string]struct{}{
    "apple": {}, "banana": {}, "cherry": {},
}}

for item := range set.All() {
    fmt.Println(item)
}

進階應用

1. Pull 函數 - 手動控制迭代

func demonstratePull() {
    seq := rangeSeq(1, 5)

    // 轉換為 Pull 迭代器
    next, stop := iter.Pull(seq)
    defer stop() // 重要:必須調用 stop 來清理資源

    // 手動拉取值
    for {
        value, ok := next()
        if !ok {
            break // 序列結束
        }
        fmt.Printf("Pulled: %d\n", value)

        // 可以在這裡添加複雜的邏輯
        if value == 3 {
            fmt.Println("Stopping early")
            break
        }
    }
}

實際用途:

  • API 分頁處理:需要根據回應決定是否繼續

  • 資料庫批次讀取:根據記憶體使用量控制讀取

  • 檔案處理:逐行處理大檔案,可隨時停止

2. 配對迭代器

// 將序列中的值配對
func Pairs[V any](seq iter.Seq[V]) iter.Seq2[V, V] {
    return func(yield func(V, V) bool) {
        next, stop := iter.Pull(seq)
        defer stop()

        for {
            v1, ok1 := next()
            if !ok1 {
                return
            }

            v2, ok2 := next()
            // 如果 ok2 為 false,v2 是零值;產生最後一對
            if !yield(v1, v2) {
                return
            }

            if !ok2 {
                return
            }
        }
    }
}

// 使用範例:座標處理
coordinates := []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}
coordSeq := SliceSeq(coordinates)

for x, y := range Pairs(coordSeq) {
    point := Point{X: x, Y: y}
    drawPoint(point)
}
// 結果:(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)

3. Filter 和 Map 操作

// Filter 迭代器
func Filter[V any](seq iter.Seq[V], predicate func(V) bool) iter.Seq[V] {
    return func(yield func(V) bool) {
        for value := range seq {
            if predicate(value) {
                if !yield(value) {
                    return
                }
            }
        }
    }
}

// Map 迭代器
func Map[T, U any](seq iter.Seq[T], mapper func(T) U) iter.Seq[U] {
    return func(yield func(U) bool) {
        for value := range seq {
            if !yield(mapper(value)) {
                return
            }
        }
    }
}

// 鏈式操作
numbers := rangeSeq(1, 10)
evenNumbers := Filter(numbers, func(n int) bool { return n%2 == 0 })
squares := Map(evenNumbers, func(n int) int { return n * n })

for square := range squares {
    fmt.Printf("%d ", square) // 4 16 36 64 100
}

4. 無限序列

// 費波那契數列
func Fibonacci() iter.Seq[int] {
    return func(yield func(int) bool) {
        a, b := 0, 1
        for {
            if !yield(a) {
                return
            }
            a, b = b, a+b
        }
    }
}

// 使用時需要手動控制停止
func useFibonacci() {
    for num := range Fibonacci() {
        if num > 1000 {
            break
        }
        fmt.Println(num)
    }
}

// 產生唯一 ID
func UniqueIDs() iter.Seq[string] {
    return func(yield func(string) bool) {
        counter := 0
        for {
            id := fmt.Sprintf("ID_%d_%d", time.Now().Unix(), counter)
            if !yield(id) {
                return
            }
            counter++
        }
    }
}

實際用途:

  • 測試資料:生成無限測試資料

  • 演算法實現:實現需要無限序列的演算法

5. 批次處理

// 將序列分批處理
func Batch[T any](seq iter.Seq[T], size int) iter.Seq[[]T] {
    return func(yield func([]T) bool) {
        batch := make([]T, 0, size)

        for item := range seq {
            batch = append(batch, item)
            if len(batch) == size {
                if !yield(batch) {
                    return
                }
                batch = batch[:0] // 重用 slice
            }
        }

        // 處理剩餘元素
        if len(batch) > 0 {
            yield(batch)
        }
    }
}
  1. 常用輔助函數

// 將 slice 轉換為 Iterator
func SliceSeq[T any](slice []T) iter.Seq[T] {
    return func(yield func(T) bool) {
        for _, item := range slice {
            if !yield(item) {
                return
            }
        }
    }
}

// 將 Iterator 收集為 slice
func Collect[T any](seq iter.Seq[T]) []T {
    var result []T
    for item := range seq {
        result = append(result, item)
    }
    return result
}

// 取前 n 個元素
func Take[T any](seq iter.Seq[T], n int) iter.Seq[T] {
    return func(yield func(T) bool) {
        count := 0
        for item := range seq {
            if count >= n {
                return
            }
            if !yield(item) {
                return
            }
            count++
        }
    }
}

// 跳過前 n 個元素
func Skip[T any](seq iter.Seq[T], n int) iter.Seq[T] {
    return func(yield func(T) bool) {
        count := 0
        for item := range seq {
            if count < n {
                count++
                continue
            }
            if !yield(item) {
                return
            }
        }
    }
}

命名慣例

根據官方文件,Iterator 的命名有以下慣例:

1. 集合迭代器

// 集合類型的主要迭代器命名為 All
func (s *Set[V]) All() iter.Seq[V] { ... }

// 多種序列時,名稱指示具體序列
func (c *Country) Cities() iter.Seq[*City] { ... }
func (c *Country) Languages() iter.Seq[string] { ... }

2. 配置參數的迭代器

// 需要額外配置的迭代器
func (m *Map[K, V]) Scan(min, max K) iter.Seq2[K, V] { ... }
func Split(s, sep string) iter.Seq[string] { ... }

3. 不同順序的迭代器

// 指示遍歷順序
func (l *List[V]) All() iter.Seq[V] { ... }        // 從頭到尾
func (l *List[V]) Backward() iter.Seq[V] { ... }   // 從尾到頭
func Preorder(root Node) iter.Seq[Node] { ... }    // 前序遍歷

性能考量

延遲求值的優勢

// 延遲求值範例:只計算需要的部分
func expensiveComputation() iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 1; i <= 1000000; i++ {
            // 昂貴的計算
            result := complexCalculation(i)
            if !yield(result) {
                return // 可以提早停止,節省計算
            }
        }
    }
}

// 只取前 5 個結果,剩下的不會計算
results := Take(expensiveComputation(), 5)
for result := range results {
    fmt.Println(result)
}

記憶體使用效率


// 傳統方式:需要將所有結果載入記憶體
func processAllAtOnce() []ProcessedData {
    data := loadLargeDataset() // 可能很大
    var results []ProcessedData
    for _, item := range data {
        results = append(results, process(item))
    }
    return results // 記憶體使用量可以說"翻倍"
}

// Iterator 方式:串流處理
// 用到哪,拉取到哪
func processStream() iter.Seq[ProcessedData] {
    return func(yield func(ProcessedData) bool) {
        for item := range loadDataStream() {
            processed := process(item)
            if !yield(processed) {
                return
            }
        }
    }
}

推薦的錯誤處理方式

// 方法 1:使用 Result 類型
type Result[T any] struct {
    Value T
    Error error
}

func ProcessWithErrors[T, U any](seq iter.Seq[T], processor func(T) (U, error)) iter.Seq[Result[U]] {
    return func(yield func(Result[U]) bool) {
        for item := range seq {
            value, err := processor(item)
            if !yield(Result[U]{Value: value, Error: err}) {
                return
            }
        }
    }
}

// 方法 2:分離錯誤處理
func ProcessSafe[T, U any](seq iter.Seq[T], processor func(T) (U, error)) (iter.Seq[U], error) {
    var firstError error

    filtered := func(yield func(U) bool) {
        for item := range seq {
            value, err := processor(item)
            if err != nil {
                if firstError == nil {
                    firstError = err
                }
                continue
            }
            if !yield(value) {
                return
            }
        }
    }

    return filtered, firstError
}

與傳統 Iterator Pattern 的比較

相同點

  • 目的相同:提供統一的方式遍歷集合

  • 封裝性:隱藏集合內部實現

  • 順序訪問:按順序訪問元素

主要差異

特性傳統 Iterator PatternGo Iterator
實現方式物件導向(介面 + 類別)函數式(closure + yield)
狀態管理Iterator 物件維護狀態closure 維護狀態
語法支援手動調用 Next(), HasNext()原生 range 語法
記憶體開銷需要創建 Iterator 物件只有函數調用開銷
組合性較難組合多個迭代器容易組合和鏈接
類型安全依賴語言的泛型支援完整的泛型支援

傳統 Iterator Pattern

// 傳統方式
type Iterator[T any] interface {
    HasNext() bool
    Next() T
}

type SliceIterator[T any] struct {
    slice []T
    index int
}

func (it *SliceIterator[T]) HasNext() bool {
    return it.index < len(it.slice)
}

func (it *SliceIterator[T]) Next() T {
    if it.HasNext() {
        value := it.slice[it.index]
        it.index++
        return value
    }
    var zero T
    return zero
}

// 使用
func useTraditionalIterator() {
    data := []int{1, 2, 3, 4, 5}
    iterator := &SliceIterator[int]{slice: data}

    for iterator.HasNext() {
        fmt.Println(iterator.Next())
    }
}

Go Iterator

func SliceSeq[T any](slice []T) iter.Seq[T] {
    return func(yield func(T) bool) {
        for _, item := range slice {
            if !yield(item) {
                return
            }
        }
    }
}

// 使用
func useGoIterator() {
    data := []int{1, 2, 3, 4, 5}

    for item := range SliceSeq(data) {
        fmt.Println(item)
    }
}

Go 標準庫中已經實現的 Iterator

除了下述的 database Next()、filepath.Walk(Dir)sync Map Range()。其實更早之前都已經提供了 Iterator 的自我實現,也不難發現現有的 iterator 設計跟使用方式都不相同,所以 Go 才想統一標準。

Go database 的 Next()

Go 資料庫的 Next() 實現了與 Iterator 相同的迭代模式和控制流語義。

rows.Next() 的實際行為

// rows.Next() 的實際行為
func (rs *Rows) Next() bool {
    // 1. 釋放上一行的記憶體鎖定
    rs.closemuRUnlockIfHeldByScan()

    // 2. 檢查 context 是否已取消
    if rs.contextDone.Load() != nil {
        return false
    }

    // 3. 嘗試讀取下一行(這裡才真正從網路/磁碟讀取)
    var doClose, ok bool
    withLock(rs.closemu.RLocker(), func() {
        doClose, ok = rs.nextLocked() // 關鍵:這裡才讀取下一筆
    })

    // 4. 如果需要關閉就關閉
    if doClose {
        rs.Close()
    }

    return ok
}

實際的資料流與控制流

func demonstrateActualFlow() {
    // 1. Query 只是建立連線和發送 SQL,但不等待所有結果
    rows, err := db.Query("SELECT * FROM huge_table") // 只發送查詢,不讀取資料
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    fmt.Println("查詢已發送,但還沒讀取任何資料")

    // 2. 第一次 Next() 開始從緩衝區讀取,緩衝區空時才觸發網路讀取
    if rows.Next() { // 可能觸發網路讀取,填充緩衝區
        fmt.Println("讀取到第一筆資料")

        var id int
        var name string
        rows.Scan(&id, &name) // 掃描剛才讀取的資料
        fmt.Printf("第一筆: %d, %s\n", id, name)
    }

    // 3. 每次 Next() 都會讀取下一筆
    if rows.Next() { // 通常從緩衝區直接讀取
        fmt.Println("讀取到第二筆資料")
        // ... 掃描第二筆
    }

    // 4. 如果我們在這裡 early break,剩下的資料不會被讀取
    fmt.Println("停止讀取,剩下的資料不會從資料庫傳輸")
}

lib/pq 的實做方式

func networkBehavior() {
    // PostgreSQL (lib/pq)
    rows, _ := pgDB.Query("SELECT * FROM million_rows")

    // lib/pq 實際行為:
    // 1. 發送查詢到 PostgreSQL
    // 2. PostgreSQL 開始執行查詢並逐行發送結果
    // 3. TCP 將多個小的 DataRow 消息組合成 package(通常4-8KB)
    // 4. lib/pq 的 bufio.Reader 緩衝這些package
    // 5. rows.Next() 從 bufio 緩衝區逐行解析,緩衝區空時才觸發網路讀取

    count := 0
    for rows.Next() { // 每次 Next() 會:
        // - 從 bufio 緩衝區讀取並解析一行(通常很快)
        // - 當緩衝區數據用完時,觸發網路讀取下一個package(偶爾較慢)
        // - 注意:不是應用層的批次緩存,而是網路層的被動緩衝

        var data string
        rows.Scan(&data)
        count++

        if count >= 10 {
            break // PostgreSQL 可能仍在發送資料,但客戶端停止讀取
                  // 連接上可能還有未讀的網路資料
        }
    }
}

總結

🎯 核心影響

Go 1.23 引入統一的 Iterator 標準是一個重大的生態系統改進,將混亂的迭代模式統一為 iter.Seq[T]range 語法,帶來函數式程式設計能力和更好的資源效率。

主要好處

  • 統一體驗:所有迭代都用相同的 range 語法

  • 函數式能力:支援 Map、Filter、組合等操作

  • 性能優化:延遲求值、恆定記憶體使用、提早退出

  • 開發效率:降低學習成本、提升代碼可讀性

⚠️ 現實挑戰

  • 過渡期混亂:新舊兩套 API 並存,開發者需要同時掌握

  • 決策負擔:每次都要選擇用新方法還是舊方法

  • 生態系統滯後:標準庫和第三方庫需要時間適應

  • 學習資源落差:教學材料、Stack Overflow 答案新舊混雜

這是 Go 語言成熟度的重要里程碑,短期內會帶來一些混亂和學習成本,但長期來看將大幅提升 Go 的表達能力和開發體驗。關鍵是給生態系統足夠的時間來適應和標準化。

建議:新專案採用 Iterator,舊專案漸進式遷移,團隊內部制定統一的使用規範。

287 views

More from this blog

Get Your Hands Dirty on Clean Architecture CH5 Use Case

書本連結 前言 會挑這本書看的人,應該都是關心自己負責專案的軟體架構的人。不只希望開發的軟體能滿足客戶的明確需求,也希望能滿足可維護性(maintainability)的隱性需求,以及自己對結構與美觀習慣的要求。 要能滿足上述這些要求很難,因為專案通常不會按照計畫進行。可能變因有 deadline,最後的 API 與承諾的不同,又或者我們的設計無法很好的貼合需求的變化所需。。因此完美的架構只有在一

Jul 2, 20264 min read30
Get Your Hands Dirty on Clean Architecture CH5 Use Case

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read243
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南
M

MicroFIRE

74 posts