Skip to main content

Command Palette

Search for a command to run...

淺談 Go Iterator

Updated
10 min read
淺談 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,舊專案漸進式遷移,團隊內部制定統一的使用規範。

249 views

More from this blog

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

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

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

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read76
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read190
System Design Interview Ch 12 Digital Wallet

Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。 一、 動機與痛點:為什麼你需要介入 AI 的生命週期? 在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點: 記憶重置 (Session Amnesia): 痛點:每次重啟終端機,AI 就像失憶一樣。 解法:你...

Jan 24, 20266 min read470
Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦
M

MicroFIRE

71 posts