动态

详情 返回 返回

萬字長文:徹底掌握 Go 1.23 中的迭代器——使用篇 - 动态 详情

公眾號首發地址:https://mp.weixin.qq.com/s/t47eJ9rYK2CZ-hIbjx7kSg

本文帶大家一起來深入探究一下 Go 1.23 中發佈的迭代器特性,這是一篇遲來的文章,再不寫這篇文章 Go 1.25 就發佈了 :),Go 1.25 預計將於 2025 年 8 月發佈。

由於篇幅過長,所以文章拆分成了上下兩篇發佈,本文為上篇——使用篇。下篇為——原理篇,記得來讀。

何為迭代器

維基百科對迭代器的定義如下:

迭代器(英語:iterator),是使用户可在容器對象(container,例如鏈表或數組)上遍訪的對象,設計人員使用此接口無需關心容器對象的內存分配的實現細節。其行為很像數據庫技術中的光標(cursor),迭代器最早出現在1974年設計的CLU編程語言中。

在各種語言實現迭代器的方式皆不盡同,有些面嚮對象語言像Java、C#、Ruby、Python、Delphi都已將迭代器的特性內置語言當中,完美的跟語言集成,我們稱之隱式迭代器。但像是C++語言本身就沒有迭代器的特色,但STL仍利用模板實現了功能強大的迭代器。STL容器的數據的內存地址可能會重新分配(reallocate),與容器綁定的迭代器仍然可以定位到重新分配後的正確的內存地址。

雖然這段對迭代器的定義比較晦澀,但可以看出,許多主流編程語言都原生支持迭代器。而 Go 也終於在 1.23 中支持了迭代器特性,這是繼泛型特性以來 Go 在語法層面上的又一次重大更新。

我們再來看看 Go 官方是如何定義迭代器的,Go 官方在 1.23 版本新增的 iter 包文檔 中對於迭代器定義如下:

An iterator is a function that passes successive elements of a sequence to a callback function, conventionally named yield. The function stops either when the sequence is finished or when yield returns false, indicating to stop the iteration early.

翻譯如下:

迭代器是一個將序列的連續元素傳遞給回調函數(通常命名為 yield)的函數。該函數在序列結束或 yield 返回 false(表示提前停止迭代)時停止。

看到這個定義,你可能還不是很理解,不過沒關係,你現在只需要記得迭代器是一個函數即可,接下來我們將通過此文徹底掌握 Go 迭代器。

為什麼要引入迭代器

在 discussions/56413 中 Go 團隊技術負責人 rsc 對 Go 中為什麼要引入迭代器做了詮釋:

In the standard library alone, we have archive/tar.Reader.Next, bufio.Reader.ReadByte, bufio.Scanner.Scan, container/ring.Ring.Do, database/sql.Rows, expvar.Do, flag.Visit, go/token.FileSet.Iterate, path/filepath.Walk, go/token.FileSet.Iterate, runtime.Frames.Next, and sync.Map.Range, hardly any of which agree on the exact details of iteration. Even the functions that agree on the signature don’t always agree about the semantics. For example, most iteration functions that return (T, bool) follow the usual Go convention of having the bool indicate whether the T is valid. In contrast, the bool returned from runtime.Frames.Next indicates whether the next call will return something valid.

大概意思是説,僅標準庫中,就包含這麼多迭代器函數,並且這些函數接口並未統一,各自為政。甚至有些迭代器即使在函數簽名上達成了一致,在語義上卻各不相同。這就使得我們學習 Go 代碼的成本提高了,而且與 Go 語言面向工程的簡潔特點相悖。

於是,這個由 rsc 牽頭,社區中爭議很大的特性在 Go 1.23 中落地了。

這裏舉幾個例子,你可以先來感受一下 Go 語言在未提供迭代器特性時的現狀:

// bufio.Reader
r := bufio.NewReader(...)
for {
    line, _ , err := r.ReadLine()
    if err != nil {
        break
    }
    // do something
}


// bufio.Scanner
scanner := bufio.NewScanner(...)
for scanner.Scan() {
    line := scanner.Text()
    // do something
}


// database/sql.Rows
rows, _ := db.QueryContext(...)
for rows.Next() {
    if err := rows.Scan(...); err != nil {
        break
    }
    // do something
}

可以發現,Go 語言中迭代器函數的設計非常混亂,接口設計各不相同,這些設計可能都是當時的最佳實踐,但是接口不統一的事實,確實存在問題,急需一個統一的標準來解決此問題。

所以,其實迭代器在 Go 語言中並不是什麼新鮮的東西,它們一直存在,只不過各個迭代器函數實現接口並不統一。這個問題早期也許不明顯,但隨着 Go 語言標準庫功能的增多以及泛型特性的引入,越來越多的泛型集合實現,也都需要設計迭代器接口。因此,語法層面的迭代器特性呼之欲出。

現在,你大概理解什麼是迭代器了嗎?你可以簡單的,將能夠作用於 for 循環,並不斷產生下一個值的對象,稱為迭代器。

迭代器示例

我們已經對迭代器有了初步的認識,為了加深你對 Go 中迭代器的理解,現在我們一起來自己實現一下迭代器。

迭代器模式

Go 是現代語言中的新貴,雖然與主流面嚮對象語言在設計上有所出入,不過 Go 也支持通過設計模式中的迭代器模式來實現迭代器特性。

示例如下:

package main

import "fmt"

type Iterator struct {
    data  []int
    index int
}

func NewIterator(data []int) *Iterator {
    return &Iterator{data: data, index: 0}
}

func (it *Iterator) HasNext() bool {
    return it.index < len(it.data)
}

func (it *Iterator) Next() int {
    if !it.HasNext() {
        panic("Stop iteration")
    }
    value := it.data[it.index]
    it.index++
    return value
}

func main() {
    it := NewIterator([]int{0, 1, 2, 3, 4})
    for it.HasNext() {
        fmt.Println(it.Next())
    }
}

這是 Go 語言實現的簡化版本的迭代器模式,NewIterator 創建並返回一個迭代器對象,it.HasNext() 返回迭代器中是否還有下一個值,剛好可以作用於 for 循環對其進行遍歷,it.Next() 返回迭代器中的下一個值。

回調函數風格

此外,因為 Go 語言支持高階函數,即函數可以作為另一個函數的參數,所以我們還可以通過回調函數的方式,來實現迭代器。

示例如下:

package main

import (
    "container/ring"
    "fmt"
)

func main() {
    // 循環鏈表
    r := ring.New(5)
    // 初始化鏈表
    for i := 0; i < r.Len(); i++ {
        r.Value = i  // 為當前節點賦值
        r = r.Next() // 移動到下一個節點
    }
    // 迭代器
    r.Do(func(v any) {
        fmt.Println(v)
    })
}

這裏引入了標準庫中的 container/ring 包,這個包實現了循環鏈表,它定義了 *Ring.Do 方法來對鏈表 Ring 對象進行遍歷操作。

Do 方法按正向順序對鏈表中的每個元素調用函數 f,其實現如下:

// Do calls function f on each element of the ring, in forward order.
// The behavior of Do is undefined if f changes *r.
func (r *Ring) Do(f func(any)) {
    if r != nil {
        f(r.Value)
        for p := r.Next(); p != r; p = p.next {
            f(p.Value)
        }
    }
}

可以看到,Do 方法內部有一個 for 循環,不停的調用 r.Next() 來獲取鏈表中的下一個值,然後調用回調函數 f 並將下一個值傳遞給它。

Go 風格迭代器

以上兩種方式實現的迭代器,在其他主流編程語言中也很容易復刻。現在我們再來使用 channel 數據結構實現一種 Go 語言獨有風格的迭代器。

示例如下:

package main

import "fmt"

func generator(n int) <-chan int {
    ch := make(chan int)
    go func() {
        for i := 0; i < n; i++ {
            ch <- i
        }
        close(ch)
    }()
    return ch
}

func main() {
    for n := range generator(5) {
        fmt.Println(n)
    }
}

這裏我們定義了一個生成器函數 generator,它返回一個只讀的 <-chan int。而 channel 類型恰好能夠作用於 for-range 循環,生成器 generator 不斷產生下一個值,for-range 循環就能接收到,這樣我們就實現了 Go 風格的迭代器。

雖然這個版本的迭代器非常具有 Go 風格,但因為使用了 channel 所以性能較差,不建議作為實現迭代器的首選方案。

有了前面的鋪墊,接下來我們就要真正進入到 Go 語言官方推出的迭代器的學習了。

何時引入迭代器

Go 引入迭代器的過程可謂一波多折,在 2020 年 8 月的 issues/40605 中就有人為 Go 2 提出了迭代器提案,之後又經歷了在 issues/43557、discussions/54245、discussions/56413、issues/61405、issues/61897 中多次討論。終於,迭代器特性以實驗性質被加入到 Go 1.22 版本中,通過 GOEXPERIMENT=rangefunc 編譯參數可以在 Go 中啓用 range-over-function 迭代器特性。此外,rsc 還在 issues/61898 中提出了 golang.org/x/exp/xiter 包的提案,用於適配迭代器,不過這個提案最終被撤銷了。

2024 年 8 月,隨着 Go 1.23 版本的發佈,Go 迭代器也終於正式落地了。同時引入了 iter 包,來為用户自定義迭代器提供便利。

這便是 Go 引入迭代器的主要時間脈絡。

改變

Go 語言發佈之初,for-range 循環就能夠支持遍歷 arrayslicestringmapchannel 幾種內置類型。

在 Go 1.22 中,for-range 新增了對整數類型值(integer value)的支持,比如 for i := range 10 {...}

而在 Go 1.23 中,Go 引入了迭代器特性,for-range 又新增了對如下三種特定函數類型的支持:

// 無返回值迭代器,函數 f 簽名為:func(func() bool)
for range f {...}

// 返回一個值迭代器,函數 f 簽名為:func(func(V) bool)
for x := range f { ... }

// 返回兩個值迭代器,函數 f 簽名為:func(func(K, V) bool)
for x, y := range f { ... }

現在,for-range 支持遍歷的所有類型如下:

https://go.dev/ref/spec#For_range
Range expression                                       1st value                2nd value

array or slice      a  [n]E, *[n]E, or []E             index    i  int          a[i]       E
string              s  string type                     index    i  int          see below  rune
map                 m  map[K]V                         key      k  K            m[k]       V
channel             c  chan E, <-chan E                element  e  E
integer value       n  integer type, or untyped int    value    i  see below
function, 0 values  f  func(func() bool)
function, 1 value   f  func(func(V) bool)              value    v  V
function, 2 values  f  func(func(K, V) bool)           key      k  K            v          V

至此,Go 語言 for-range 語句支持的可迭代對象類型完整拼圖已經形成,真正實現了“萬物皆可遍歷”的能力。

迭代器使用示例

基於以上的講解,你可能對 Go 迭代器還是有些不明所以,那麼現在,咱們一起從代碼層面,切身感受一下迭代器的存在。我將通過幾個示例代碼,帶你體驗迭代器的用法。

迭代器實現

最簡單的迭代器

我們先來實現一個最簡單的迭代器。

示例如下:

package main

import (
    "fmt"
)

func iterator(yield func() bool) {
    for i := 0; i < 5; i++ {
        if !yield() {
            return
        }
    }
}

func main() {
    i := 0
    for range iterator {
        fmt.Printf("i=%d\n", i)
        i++
    }
}

iterator 函數就是一個迭代器,它接收一個 yield 函數作為參數,並且函數簽名符合 func(func() bool) 類型,那麼 iterator 就能夠作用於 for-range 循環。

iterator 函數內部,我們啓動了一個 for 循環,這個循環將會迭代 5 次。並且在 for 循環代碼塊內部,每次迭代都會調用 yield() 函數,並判斷其返回值,如果返回 false,則直接使用 return 提前終止循環。

執行示例代碼,得到輸出如下:

$ go run main.go
i=0
i=1
i=2
i=3
i=4

這個示例看起來有點傻,但這確實已經是 Go 中最簡單的迭代器實現了。

你也許比較疑惑,這個示例代碼的執行邏輯是什麼?咱們暫且不去深究原理,挖一個坑留在這裏,稍後再來填上。

現在,你只需要知道這段代碼大概按照如下流程來執行:

  • 首先,符合函數簽名 func(func() bool) 的函數,都可以作為迭代器,所以 iterator 是一個迭代器,可以應用於 for-range 循環。
  • 接着,當 for-range 開始迭代時,迭代器函數 iterator 會被調用,並執行其內部代碼。這裏你需要注意,iterator 函數只會被調用一次。
  • 現在重點來了,iterator 函數接收一個 yield 函數作為參數,那麼這個 yield 函數是哪裏來的呢?其實它是 Go 編譯器幫我們自動生成的函數,並自動作為參數傳遞給 iteratoryield 函數內部包含了 for-range 循環體中的代碼,你可以簡單的將 yield 函數理解為如下偽代碼:
func yield() bool {
    fmt.Printf("i=%d\n", i)
    i++
    return true
}
  • iterator 函數內部,有一個 for 循環,將執行 5 次。每一次循環都會調用 yield 函數,並根據其返回值決定是否停止迭代。
  • for 循環一旦結束,iterator 函數便執行完成,即 for-range 迭代完成。

現在,是不是對迭代器的認知更清晰了一些呢?

控制迭代次數

剛剛的迭代器實現過於簡單,甚至都無法從外部控制迭代次數,為了控制迭代次數,我們可以給 iterator 再嵌套一層函數。

示例如下:

package main

import (
    "fmt"
)

func iterator(n int) func(yield func() bool) {
    return func(yield func() bool) {
        for i := 0; i < n; i++ {
            if !yield() {
                return
            }
        }
    }
}

func main() {
    i := 0
    for range iterator(3) {
        fmt.Printf("i=%d\n", i)
        i++
    }
}

這裏為 iterator 又嵌套了一層函數,這樣就可以通過參數的形式控制迭代次數了。

執行示例代碼,得到輸出如下:

$ go run main.go
i=0
i=1
i=2

值得注意的是,現在 for-range 遍歷迭代器時,不再是直接使用函數名稱 for range iterator 的形式進行迭代,而是要調用 iterator 函數以 for range iterator(3) 的形式來迭代。所以,實際上 iterator(3) 的返回值,才是真正的迭代器函數。

這個示例代碼的套路是不是有點熟悉?用 Go 寫過 Web 程序的讀者應該能夠體會得到,比如我們在用 Gin 框架開發項目時,編寫的中間件程序就經常使用這種套路。如開源項目 miniblog 中的 AuthnMiddleware 中間件實現。

輸出一個值

我們已經展示了兩個迭代器示例,但是,它們都不產生值,在使用 for-range 進行迭代時,真的就是純迭代,for-range 接收不到任何循環變量,所以感覺也沒啥大用。

那麼我們再來實現一個能夠在每輪迭代時輸出一個值的迭代器。

示例如下:

package main

import (
    "fmt"
)

func iterator(n int) func(yield func(v int) bool) {
    return func(yield func(v int) bool) {
        for i := 0; i < n; i++ {
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    i := 0
    for v := range iterator(10) {
        if i >= 5 {
            break
        }
        fmt.Printf("%d => %d\n", i, v)
        i++
    }
}

這個版本的迭代器函數簽名已經變了,不再是 func(func() bool),而變成了 func(func(V) bool)。即 yield 函數支持接收一個參數 v int

執行示例代碼,得到輸出如下:

$ go run main.go
0 => 0
1 => 1
2 => 2
3 => 3
4 => 4

現在這個示例程序終於看起來有點意義了,像那麼回事了。

輸出兩個值

講到這裏,想必你已經猜到接下來我們要實現的迭代器功能了。

沒錯,for-range 在迭代容器類型數據結構時,是支持接收兩個循環變量的。比如迭代 slice 時會產生 indexvalue 兩個循環變量,迭代 map 時會產生 keyvalue 兩個循環變量。

我們先來實現支持迭代 slice 類型的迭代器。

示例如下:

package main

import (
    "fmt"
)

func iterator(slice []int) func(yield func(i, v int) bool) {
    return func(yield func(i int, v int) bool) {
        for i, v := range slice {
            if !yield(i, v) {
                return
            }
        }
    }
}

func main() {
    s := []int{0, 1, 2, 3, 4}
    for i, v := range iterator(s) {
        if i == 2 {
            continue
        }
        fmt.Printf("%d => %d\n", i, v)
    }
}

這個迭代器函數簽名為 func(func(K, V) bool),即支持輸出兩個值,分別是 slice 對象索引(i)和元素(v)。

執行示例代碼,得到輸出如下:

$ go run main.go
0 => 0
1 => 1
3 => 3
4 => 4

根據輸出,可以發現我們正確的得到了 indexvalue。並且,continue 語句也可以正常應用於迭代器。

迭代 map

最後,我們再來來實現一下支持迭代 map 類型的迭代器。

示例如下:

package main

import (
    "fmt"
)

func iterator(m map[string]int) func(yield func(k string, v int) bool) {
    return func(yield func(k string, v int) bool) {
        for k, v := range m {
            if !yield(k, v) {
                return
            }
        }
    }
}

func main() {
    m := map[string]int{
        "a": 0,
        "b": 1,
        "c": 2,
    }
    for k, v := range iterator(m) {
        fmt.Printf("%s: %d\n", k, v)
    }
}

可以發現,這裏其實並沒有什麼新的內容,甚至迭代器函數簽名都沒變 func(func(K, V) bool),只不過 KV 的類型變了。只需簡單改造,我們就已經實現了支持迭代 map 類型的迭代器。

執行示例代碼,得到輸出如下:

$ go run main.go
b: 1
c: 2
a: 0

本小結,我們一起實現了幾個迭代器。這些迭代器看起來有點用,但好像也沒那麼有用。而且,僅從示例程序來看,我們好像把事情變得更加複雜了。

如你所見,目前來看的確如此。比如迭代 slice 對象,只需要用 for i, v := range slice 即可,為什麼還需要構造一個迭代器呢?

咱們接着往下看,你會得到答案。

泛型版本

初次接觸 Go 迭代器時,你可能也和我一樣,有點雲裏霧裏,Get 不到其用意。

現在,我們使用泛型來重新實現一遍這幾個迭代器函數,相信你會對迭代器的用途有更深入的理解。

輸出零個值

以下是泛型版本最簡單的迭代器實現。

示例如下:

package main

import (
    "fmt"
)

type Seq0 func(yield func() bool)

func iter0[Slice ~[]E, E any](s Slice) Seq0 {
    return func(yield func() bool) {
        for range s {
            if !yield() {
                return
            }
        }
    }
}

func main() {
    s1 := []int{1, 2, 3}
    i := 0
    for range iter0(s1) {
        fmt.Printf("i=%d\n", i)
        i++
    }

    fmt.Println("--------------")

    s2 := []string{"a", "b", "c"}
    i = 0
    for range iter0(s2) {
        fmt.Printf("i=%d\n", i)
        i++
    }
}

為了讓代碼可讀性更好一些,我將迭代器函數簽名定義為 Seq0 類型,命名中的 0 表示迭代器輸出零個值。

為了體現泛型的作用,這裏還分別演示了迭代 intstring 兩種類型的 slice 對象 s1s2

執行示例代碼,得到輸出如下:

$ go run main.go
i=0
i=1
i=2
--------------
i=0
i=1
i=2
輸出一個值

同樣是迭代 slice 對象,輸出一個值的迭代器實現如下:

package main

import (
    "fmt"
)

type Seq1[V any] func(yield func(V) bool)

func iter1[Slice ~[]E, E any](s Slice) Seq1[E] {
    return func(yield func(E) bool) {
        for _, v := range s {
            if !yield(v) {
                return
            }
        }
    }
}

func main() {
    s1 := []int{1, 2, 3}
    for v := range iter1(s1) {
        fmt.Printf("v=%d\n", v)
    }

    fmt.Println("--------------")

    s2 := []string{"a", "b", "c"}
    for v := range iter1(s2) {
        fmt.Printf("v=%s\n", v)
    }
}

執行示例代碼,得到輸出如下:

$ go run main.go
v=1
v=2
v=3
--------------
v=a
v=b
v=c
輸出兩個值

進一步,輸出兩個值的迭代器實現如下:

package main

import (
    "fmt"
)

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

func iter2[Slice ~[]E, E any](s Slice) Seq2[int, E] {
    return func(yield func(int, E) bool) {
        for i, v := range s {
            if !yield(i, v) {
                return
            }
        }
    }
}

func main() {
    s1 := []int{1, 2, 3}
    for i, v := range iter2(s1) {
        fmt.Printf("%d=%d\n", i, v)
    }

    fmt.Println("--------------")

    s2 := []string{"a", "b", "c"}
    for i, v := range iter2(s2) {
        fmt.Printf("%d=%s\n", i, v)
    }
}

執行示例代碼,得到輸出如下:

$ go run main.go
0=1
1=2
2=3
--------------
0=a
1=b
2=c

看到這幾個泛型版本迭代器的實現,是不是覺得 Go 的迭代器設計確實有點用。針對某種數據結構,我們只需要編寫一個迭代器函數,就能迭代此數據結構類型的所有對象。

現在,你知道如何實現泛型版本的 map 迭代器了嗎?這個實現就留給你自行去完成了。

本小結最後提醒一下,其實迭代器中的 yield 並非關鍵字,你可以隨意命名,這是隻是 Go 官方推薦的約定俗成的名字。

iter 包

前文中,我提到過,Go 隨着 1.23 迭代器的發佈,新增了 iter 包,而新的 iter 包為用户操作自定義迭代器提供了基礎定義。

如下這兩個函數類型,就是 iter 包提供的:

https://go.dev/doc/go1.23#iterators
type Seq[V any] func(yield func(V) bool)

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

也就是説,其實我們在前文中實現的迭代器,有更便捷的寫法。我們無需自己定義 SeqSeq2 類型,iter 包已經為我們定義好了。我們只需要拿過來用就行了。

示例如下:

package main

import "iter"

func iter1[Slice ~[]E, E any](s Slice) iter.Seq[E] {
    return func(yield func(E) bool) {
        for _, v := range s {
            if !yield(v) {
                return
            }
        }
    }
}

func iter2[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] {
    return func(yield func(int, E) bool) {
        for i, v := range s {
            if !yield(i, v) {
                return
            }
        }
    }
}

此外,iter 包還提供了兩個函數 PullPull2,這兩個函數簽名如下:

func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())

func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())

現在我們還無需知道它們是幹什麼的,稍後會有專門的小節講解。

Go 1.23 除了新增 iter 包,也對原有的 slices 包和 maps 進行了增強,為二者新增了很多函數方便與迭代器一起工作。

slices 包

slices 包添加了如下幾個與迭代器一起工作的函數:

  • All 返回一個遍歷切片索引和值的迭代器。
  • Values 返回一個遍歷切片元素的迭代器。
  • Backward 返回一個反向遍歷切片的迭代器(從末尾向開頭遍歷)。
  • Collect 將迭代器中的值收集到一個新切片中。
  • AppendSeq 將迭代器中的值追加到現有切片中。
  • Sorted 將迭代器中的值收集到新切片,並排序該切片。
  • SortedFunc 功能同 Sorted,但支持自定義比較函數
  • SortedStableFunc 功能同 SortedFunc,但使用穩定排序算法(保持相等元素的原始順序)。
  • Chunk 返回一個遍歷切片中連續子切片的迭代器,每個子切片最多包含 n 個元素。

源碼如下:

https://github.com/golang/go/blob/go1.23.0/src/slices/iter.go
package slices

import (
    "cmp"
    "iter"
)

// All returns an iterator over index-value pairs in the slice
// in the usual order.
func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] {
    return func(yield func(int, E) bool) {
        for i, v := range s {
            if !yield(i, v) {
                return
            }
        }
    }
}

// Backward returns an iterator over index-value pairs in the slice,
// traversing it backward with descending indices.
func Backward[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] {
    return func(yield func(int, E) bool) {
        for i := len(s) - 1; i >= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
    }
}

// Values returns an iterator that yields the slice elements in order.
func Values[Slice ~[]E, E any](s Slice) iter.Seq[E] {
    return func(yield func(E) bool) {
        for _, v := range s {
            if !yield(v) {
                return
            }
        }
    }
}

...

可以發現,這裏的 All 函數其實就是我們在前文中實現的 iter2 迭代器,Values 函數就是 iter1 迭代器。

這裏只粘貼了部分源碼,更多實現,就靠你自己去研究了。你還可以查看 https://pkg.go.dev/slices@go1.23.0 文檔,文檔中有每一個迭代器使用示例。

maps 包

maps 包添加了如下幾個與迭代器一起工作的函數:

  • All 返回一個遍歷映射中鍵值對的迭代器。
  • Keys 返回一個遍歷映射中的迭代器。
  • Values 返回一個遍歷映射中的迭代器。
  • Insert 將迭代器中的鍵值對添加到現有 map 中。
  • Collect 將迭代器中的鍵值對收集到一個新的 map 中並返回。

源碼如下:

https://github.com/golang/go/blob/go1.23.0/src/maps/iter.go
package maps

import "iter"

// All returns an iterator over key-value pairs from m.
// The iteration order is not specified and is not guaranteed
// to be the same from one call to the next.
func All[Map ~map[K]V, K comparable, V any](m Map) iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        for k, v := range m {
            if !yield(k, v) {
                return
            }
        }
    }
}

// Keys returns an iterator over keys in m.
// The iteration order is not specified and is not guaranteed
// to be the same from one call to the next.
func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K] {
    return func(yield func(K) bool) {
        for k := range m {
            if !yield(k) {
                return
            }
        }
    }
}

// Values returns an iterator over values in m.
// The iteration order is not specified and is not guaranteed
// to be the same from one call to the next.
func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V] {
    return func(yield func(V) bool) {
        for _, v := range m {
            if !yield(v) {
                return
            }
        }
    }
}

...

這裏的 All 函數就是我在前文中讓你自行去實現的能夠支持迭代 map 類型的迭代器。

對於 maps 包更深入的學習,你還可以查看 https://pkg.go.dev/maps@go1.23.0 文檔,文檔中有每一個迭代器使用示例。

現在,你對 Go 迭代器的理解是否又深入了一層呢?

有如下示例:

package main

import (
    "fmt"
    "maps"
    "slices"
)

func main() {
    s := []string{"a", "b", "c"}
    for i, v := range slices.All(s) {
        fmt.Printf("%d => %s\n", i, v)
    }

    m := map[string]int{
        "a": 0,
        "b": 1,
        "c": 2,
    }
    for k, v := range maps.All(m) {
        fmt.Printf("%s: %d\n", k, v)
    }
}

看到這段代碼,是不是感覺對迭代器的作用更加直觀了。無論是迭代 slice 還是 map 類型,我們都只需要調用 All 函數,即可完成迭代。

Go 迭代器統一了這兩種容器類型的迭代 API,後續如果再有容器類型需要迭代,都可以像這樣實現一個 All 函數。這降就低了我們開發者學習不同數據結構的迭代 API 的心智負擔。這也有助於形成共識,對用户培養相同的習慣,以後在對 Go 中任意類型的迭代,就成了一個很平常的操作,所有接口都符合直覺。

理想很豐滿,現實有點骨感,讓我們一起期待那一天的到來。

未完待續。

下篇:《徹底掌握 Go 1.23 中的迭代器——原理篇》

聯繫我

  • 公眾號:Go編程世界
  • 微信:jianghushinian
  • 郵箱:mailto:jianghushinian007@outlook.com
  • 博客:https://jianghushinian.cn
  • GitHub:https://github.com/jianghushinian

Add a new 评论

Some HTML is okay.