Go 語言新版的迭代器是怎麼個事
很多流行的編程語言中都以某種方式提供迭代器,其中包括 C++、Java、Javascript、Python 和 Rust。Go 語言現在也加入了迭代器。iter 包是 Go 1.23 新增的標準庫,提供了迭代器的基本定義和相關操作。
爲什麼需要迭代器
在 Go 1.18 引入泛型之後,便可以很方便的定義一些泛型容器類型來提升編碼效率。
例如我們可以基於 map 類型定義了一個集合類型—— Set。
// Set 基於 map 定義一個存放元素的集合類型
type Set[E comparable] struct {
m map[E]struct{}
}
// NewSet 返回一個 Set
func NewSet[E comparable]() *Set[E] {
return &Set[E]{m: make(map[E]struct{})}
}
// Add 向 Set 中添加元素
func (s *Set[E]) Add(v E) {
s.m[v] = struct{}{}
}
除了上面的集合類型,我們還可以定義很多不同的容器類型。而對於容器類型,通常都會需要遍歷其中的所有元素。但是如何實現遍歷功能,不同的容器類型、不同的人都會有不同的實現方案。
想一想在團隊協作過程中,當我們拿到一個別人定義好的容器類型,我們必須先了解一下容器的內部細節,然後才能去嘗試遍歷它。這樣我們就無法編寫一個適用於多種不同類型容器的迭代函數。因此, Go 生態系統需要有一個迭代容器元素的官方標準,讓 Gopher 們能夠儘量統一起來。
迭代器
在介紹 Go1.23 中新增的迭代器之前,我們先來看下 for/range 語句的變化。
for/range 語句改進
Go 語言內置的容器類型有數組、切片和映射。它們都有一種無需暴露底層實現即可訪問其元素的方法:for/range 語句。
for/range 語句除了適用於 Go 的內置容器類型外,還適用於字符串和通道。此外 Go 1.22 開始 for/range 語句支持 int。
從 Go 1.23 開始,for/range 語句支持單參數的函數。並且單參數本身必須是一個接收 0 到 2 個參數並返回一個 bool 的函數;按照慣例,我們稱之爲 yield 函數。
例如,下面三個常見的 yield 函數。
func(yield func() bool)
func(yield func(V) bool)
func(yield func(K, V) bool)
當我們在 Go 中談論迭代器時,我們指的是具有這三種類型之一的函數。
Go 迭代器定義
1994 年出版的《設計模式:可複用面向對象軟件的基礎》一書中介紹了 迭代器模式——"它讓用戶透過特定的接口巡訪容器中的每一個元素而不用瞭解底層的實現。"
Go 1.23 中增加的迭代器是一個函數類型,它把另一個函數( yield 函數)作爲參數,將容器中的連續元素傳遞給 yield 函數。
迭代器函數會在以下兩種情況停止。
-
在序列迭代結束後停止,表示此次迭代結束。
-
在
yield函數返回false時停止,表示提前停止迭代。
Go 標準庫 iter 包 中定義了 Seq 和 Seq2 作爲迭代器的簡稱,它們將每個序列元素的 1 或 2 個值傳遞給 yield 函數:
type (
Seq[V any] func(yield func(V) bool)
Seq2[K, V any] func(yield func(K, V) bool)
)
其中:
-
Seq是 sequence 的縮寫,因爲迭代器會循環遍歷一系列值 -
Seq2表示成對值的序列,通常是鍵值對或索引值對。
看到這裏,你應該就明白了爲什麼需要改進 for/range 語句來支持單參數函數類型了,因爲 for/range 語句要支持遍歷新增的迭代器。
Push 迭代器(標準迭代器)
實現迭代器
我們現在爲先前定義的集合類型定義一個返回所有元素的迭代器。
// All 返回一個迭代器,迭代集合中的所有元素
func (s *Set[E]) All() iter.Seq[E] {
return func(yield func(E) bool) {
for v := range s.m {
if !yield(v) {
return
}
}
}
}
上面的代碼中,對集合中的每個元素調用 yield,如果 yield 返回 false 則停止。
使用迭代器
當我們調用 s.All 後會得到一個迭代器函數,然後可以直接使用 for/range 語句來遍歷它。
func forRangeSet() {
s := NewSet[string]()
s.Add("Golang")
s.Add("Java")
s.Add("Python")
s.Add("C++")
for v := range s.All() {
fmt.Println(v)
}
}
Pull 迭代器
我們已經瞭解瞭如何在 for/range 循環中使用迭代器。但簡單的循環並不是使用迭代器的唯一方法。
例如,有時我們可能需要並行迭代兩個容器。這時我們就需要用到另外一種不同類型的迭代器:Pull 迭代器。
Push 迭代器和 Pull 迭代器的區別:
-
Push 迭代器將序列中的每個值推送到
yield函數。Push 迭代器是 Go 標準庫中的標準迭代器,並由for/range語句直接支持。 -
Pull 迭代器的工作方式則相反。每次調用 Pull 迭代器時,它都會從序列中拉出另一個值並返回該值。
for/range語句_不_直接支持 Pull 迭代器;但可以通過編寫一個簡單的 for 循環遍歷 Pull 迭代器。
通常不需要自行實現一個 Pull 迭代器,新的標準庫中 iter.Pull 和 iter.Pull2 函數能夠將標準迭代器轉爲 Pull 迭代器。
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())
它們會返回兩個函數。
-
第一個是 Pull 迭代器:每次調用時都會返回序列中的下一個值和一個布爾值,該布爾值表示該值是否有效。
-
第二個是停止函數,應在完成 Pull 迭代器後調用。
Pull 迭代器示例,將一個迭代器中的兩個連續值對作爲一個元素,返回一個新的迭代器。
// Pairs 返回一個迭代器,遍歷 seq 中連續的值對。
func Pairs[V any](seq iter.Seq[V]) iter.Seq2[V, V] {
returnfunc(yield func(V, V) bool) {
next, stop := iter.Pull(seq)
defer stop()
for {
v1, ok1 := next()
if !ok1 {
return
}
v2, ok2 := next()
// If ok2 is false, v2 should be the
// zero value; yield one last pair.
if !yield(v1, v2) {
return
}
if !ok2 {
return
}
}
}
}
迭代器實踐
命名約定
迭代器函數或者方法通常根據被遍歷的序列命名,例如用 All 方法返回序列中所有的元素。
// All 返回 s 中所有元素的迭代器。
func (s *Set[V]) All() iter.Seq[V]
對於包含多個可能序列的類型,迭代器的名稱需要能夠指明正在提供的是哪個序列:
// Country 國家
type Country struct {
cities []*City // 主要城市
languages []string// 官方語言
}
// City 城市
type City struct {
Name string
Code int
}
// Cities 返回一個迭代器,迭代該國家的主要城市。
func (c *Country) Cities() iter.Seq[*City] {
returnfunc(yield func(city *City) bool) {
for _, v := range c.cities {
if !yield(v) {
return
}
}
}
}
// Languages 返回一個迭代器,迭代該國家的官方語言。
func (c *Country) Languages() iter.Seq[string] {
returnfunc(yield func(language string) bool) {
for _, v := range c.languages {
if !yield(v) {
return
}
}
}
}
一次性迭代器(Single-Use Iterators)
通常迭代器都會支持多次遍歷,但是在某些場景下,例如,從網絡或文件讀取字節流時,迭代結束後就不會再讀到值了。
這種返回一次性迭代器的函數或方法需要在文檔註釋中標明。
// Lines 返回一個從 r 按行讀取的迭代器
// 返回的是一次性迭代器
func (r *Reader) Lines() iter.Seq[string]
標準庫使用示例
標準庫中的一些包已經提供了基於迭代器的 API,最常見的是 maps 包和 slices 包。
例如,maps.Keys 返回一個映射中所有鍵的迭代器,而 slices.Sorted 將迭代器的值收集到一個切片中,對它們進行排序,並返回排序後的切片。
func sortDomo() {
m := map[int]string{
1: "liwenzhou.com",
3: "q1mi.cn",
2: "go.dev",
}
for _, key := range slices.Sorted(maps.Keys(m)) {
fmt.Println(key)
}
}
適配器
迭代器的標準定義的一個優點是能夠編寫使用它們的標準適配器函數。
例如,可以定義一個過濾值序列並返回新序列的 Filter 函數。
這個 Filter 函數接受迭代器作爲參數,並返回一個新的迭代器。它另一個參數是一個過濾器函數,它決定過濾器返回的新迭代器中應該過濾掉哪些值。
// Filter 返回一個迭代器序列,過濾掉 s 中 f(v) 爲 false 的值
func Filter[V any](f func(V) bool, s iter.Seq[V]) iter.Seq[V] {
returnfunc(yield func(V) bool) {
for v := range s {
if f(v) {
if !yield(v) {
return
}
}
}
}
}
func filterDemo() {
m := map[int]string{
1: "liwenzhou.com",
3: "q1mi.cn",
2: "go.dev",
}
isLong := func(v string) bool {
returnlen(v) > 6
}
for _, key := range slices.Collect(Filter(isLong, maps.Values(m))) {
fmt.Println(key)
}
}
在這個例子中,你也完全可以使用 for 循環來過濾掉不符合要求的元素,這樣代碼可能會更清晰;但是使用一個通用的 Filter 函數配合迭代器可以讓代碼更通用。
直接傳遞函數給迭代器
對於一些簡單的場景,比如只是想打印下元素,我們甚至可以不使用 for/range 語句遍歷迭代器。可以直接將函數當成參數傳遞給迭代器。例如上面打印集合中元素的示例,也可以寫成下面的形式。
func forRangeSet() {
s := NewSet[string]()
s.Add("Golang")
s.Add("Java")
s.Add("Python")
s.Add("C++")
//for v := range s.All() {
// fmt.Println(v)
//}
// 直接把函數傳入迭代器
s.All()(func(v string) bool {
fmt.Println(v)
returntrue
})
}
這裏,我們相當於傳遞了一個自定義的 yield 函數。
總結
雖然看起來 Go 裏面的迭代器很複雜(有太多的嵌套),但是隻要理解了 yield 函數的作用,其原理還是比較簡單的。
至於執行效率的問題,Go 編譯器和運行時中做了相當多的工作來提高效率,並正確處理循環中的 break 或 panic。
參考鏈接:
-
https://go.dev/blog/range-functions
-
https://pkg.go.dev/iter
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/5yMouzI6qNL0PhAqI1WdZg