Go 1.23 迭代器:range over func 的革命

深入探索 Go 1.23 的迭代器特性:range over function、iter 包、Seq/Seq2 模式、惰性求值、与 channel 的对比、性能分析

Go 1.23 迭代器:range over func 的革命

Go 语言有一件事一直让人又爱又恨:for range 循环。

爱它是因为它简洁、安全、没有 C 语言那种 for (int i = 0; i < n; i++) 的啰嗦;恨它是因为它能遍历的东西太少了——数组、切片、map、字符串、channel,就这几样。

如果你想遍历一棵二叉树、遍历一个数据库游标、或者遍历一个无限序列,你只能……写一个丑陋的 for 循环,或者把数据全部收集到切片里再 range

这一切在 Go 1.23 中彻底改变了。

Go 1.23 引入了 range over function 特性——现在你可以让 for range 遍历一个函数。这个看似简单的改变,可能是 Go 自泛型以来最重要的语法扩展。它打开了"惰性求值"的大门,让 Go 终于拥有了现代编程语言标配的迭代器模式。

从一个问题说起

假设你有一个很大的日志文件,你想逐行读取并处理。传统做法:

package main

import (
    "bufio"
    "fmt"
    "os"
)

// 方案 1:把所有行读进内存(❌ 大文件会爆内存)
func readAllLines(filename string) ([]string, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    var lines []string
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        lines = append(lines, scanner.Text())
    }
    return lines, scanner.Err()
}

// 方案 2:用 channel 流式处理(❌ 需要 goroutine,有额外开销)
func streamLines(filename string) (<-chan string, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }

    ch := make(chan string)
    go func() {
        defer file.Close()
        defer close(ch)
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            ch <- scanner.Text()
        }
    }()

    return ch, nil
}

func main() {
    // 方案 1:内存爆炸
    lines, _ := readAllLines("huge.log")
    for _, line := range lines {
        fmt.Println(line)
    }

    // 方案 2:需要 goroutine
    ch, _ := streamLines("huge.log")
    for line := range ch {
        fmt.Println(line)
    }
}

两种方案都不理想。方案 1 在文件很大时会 OOM;方案 2 引入了 goroutine 和 channel,增加了复杂度和开销。

Go 1.23 的迭代器给出了第三种方案:

// 方案 3:迭代器(✅ 惰性求值,无 goroutine 开销)
func Lines(filename string) iter.Seq2[string, error] {
    return func(yield func(string, error) bool) {
        file, err := os.Open(filename)
        if err != nil {
            yield("", err)
            return
        }
        defer file.Close()

        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            if !yield(scanner.Text(), nil) {
                return // 调用者 break 了
            }
        }
        if err := scanner.Err(); err != nil {
            yield("", err)
        }
    }
}

func main() {
    for line, err := range Lines("huge.log") {
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(line)
    }
}

看到了吗?for line, err := range Lines("huge.log")——range 现在可以遍历一个函数了!这个函数每次只生成一行数据,调用者 break 时它会立即停止。没有 goroutine,没有 channel,没有内存爆炸。

这就是迭代器的威力。

什么是 range over function?

在 Go 1.23 之前,range 只能用于这些类型:

for i, v := range slice {}      // 切片
for k, v := range myMap {}      // map
for i, ch := range "hello" {}   // 字符串
for i := 0; i < 10; i++ {}      // 整数(Go 1.22 新增)
for v := range ch {}            // channel

Go 1.23 新增了一种:

for v := range someFunc {}      // 函数!

但并不是任何函数都能被 range。它必须是以下三种签名之一:

func(yield func() bool)                       // 无值迭代器
func(yield func(V) bool)                      // 单值迭代器:iter.Seq[V]
func(yield func(K, V) bool)                   // 双值迭代器:iter.Seq2[K, V]

这三种类型定义在 iter 包里:

package iter

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

最简单的迭代器:从零开始

让我们从最简单的例子开始,理解迭代器的工作原理。

一个生成自然数的迭代器

package main

import (
    "fmt"
    "iter"
)

// Count 生成从 start 开始的无限序列
func Count(start int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := start; ; i++ {
            if !yield(i) {
                return // 调用者 break 或 return 了
            }
        }
    }
}

func main() {
    // 只取前 5 个
    count := 0
    for v := range Count(1) {
        fmt.Println(v) // 1, 2, 3, 4, 5
        count++
        if count >= 5 {
            break
        }
    }
}

执行流程剖析

很多人第一次看迭代器会觉得晕:这个 yield 到底是什么?它是怎么"跳回"循环体的?

让我用一个详细注释的版本来解释:

func main() {
    // 步骤 1:Go 运行时调用 Count(1) 返回的函数
    // 步骤 2:这个函数内部开始执行 for 循环
    // 步骤 3:i = 1 时,调用 yield(1)
    // 步骤 4:yield(1) 把 1 "传回"给 for range 循环体
    // 步骤 5:循环体打印 1
    // 步骤 6:循环体结束,yield 返回 true(表示继续)
    // 步骤 7:回到迭代器函数,i++,i = 2
    // 步骤 8:调用 yield(2)... 重复

    for v := range Count(1) {
        fmt.Println(v)
        if v >= 5 {
            break // 步骤 N:break 导致 yield 返回 false
                  // 迭代器函数收到 false,return 退出
        }
    }
}

关键洞察yield 是一个神奇的"传送门"——调用它时,控制权会从迭代器函数传回for range 的循环体;循环体执行完毕(或 break)后,控制权又传回迭代器函数。

这种"你来我往"的控制流切换,是通过编译器生成的协程(coroutine)机制实现的——但不需要你启动真正的 goroutine。这是编译器层面的魔法,开销非常小。

iter 包:Seq 和 Seq2

iter 包定义了两个核心类型:

Seq[V]:单值迭代器

package main

import (
    "fmt"
    "iter"
)

// Fibonacci 斐波那契数列迭代器
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 main() {
    // 打印前 10 个斐波那契数
    count := 0
    for v := range Fibonacci() {
        fmt.Printf("F(%d) = %d\n", count, v)
        count++
        if count >= 10 {
            break
        }
    }
}
// 输出:
// F(0) = 0
// F(1) = 1
// F(2) = 1
// F(3) = 2
// F(4) = 3
// F(5) = 5
// F(6) = 8
// F(7) = 13
// F(8) = 21
// F(9) = 34

Seq2[K, V]:双值迭代器

package main

import (
    "fmt"
    "iter"
)

// Pairs 生成键值对
func Pairs[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
            }
        }
    }
}

// Enumerate 给任何 Seq 添加索引
func Enumerate[V any](seq iter.Seq[V]) iter.Seq2[int, V] {
    return func(yield func(int, V) bool) {
        i := 0
        for v := range seq {
            if !yield(i, v) {
                return
            }
            i++
        }
    }
}

func main() {
    // 使用 Pairs 遍历 map
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
        "Carol": 92,
    }
    for name, score := range Pairs(scores) {
        fmt.Printf("%s: %d\n", name, score)
    }

    // 使用 Enumerate 给斐波那契数列加索引
    for i, v := range Enumerate(Fibonacci()) {
        fmt.Printf("第 %d 个: %d\n", i, v)
        if i >= 5 {
            break
        }
    }
}

实战:写几个有用的迭代器

理论说够了,来点实际的。

迭代器 1:文件行读取

package main

import (
    "bufio"
    "fmt"
    "iter"
    "os"
    "strings"
)

// Lines 逐行读取文件
func Lines(filename string) iter.Seq2[string, error] {
    return func(yield func(string, error) bool) {
        file, err := os.Open(filename)
        if err != nil {
            yield("", err)
            return
        }
        defer file.Close()

        scanner := bufio.NewScanner(file)
        // 增大缓冲区以处理长行
        buf := make([]byte, 0, 64*1024)
        scanner.Buffer(buf, 1024*1024)

        for scanner.Scan() {
            if !yield(scanner.Text(), nil) {
                return
            }
        }
        if err := scanner.Err(); err != nil {
            yield("", err)
        }
    }
}

func main() {
    // 统计非空行数
    count := 0
    for line, err := range Lines("access.log") {
        if err != nil {
            fmt.Println("错误:", err)
            break
        }
        if strings.TrimSpace(line) != "" {
            count++
        }
    }
    fmt.Printf("非空行数: %d\n", count)
}

迭代器 2:数据库游标

package main

import (
    "database/sql"
    "fmt"
    "iter"
    _ "github.com/lib/pq"
)

// User 用户模型
type User struct {
    ID    int
    Name  string
    Email string
}

// QueryUsers 迭代器版本的数据库查询
func QueryUsers(db *sql.DB, query string, args ...any) iter.Seq2[User, error] {
    return func(yield func(User, error) bool) {
        rows, err := db.Query(query, args...)
        if err != nil {
            yield(User{}, err)
            return
        }
        defer rows.Close()

        for rows.Next() {
            var u User
            if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
                yield(User{}, err)
                return
            }
            if !yield(u, nil) {
                return // 调用者 break 了,提前终止查询
            }
        }
        if err := rows.Err(); err != nil {
            yield(User{}, err)
        }
    }
}

func main() {
    db, _ := sql.Open("postgres", "postgres://localhost/mydb")
    defer db.Close()

    // 只处理前 100 个活跃用户
    count := 0
    for user, err := range QueryUsers(db, "SELECT id, name, email FROM users WHERE active = $1", true) {
        if err != nil {
            fmt.Println("查询出错:", err)
            break
        }
        processUser(user)
        count++
        if count >= 100 {
            break // 迭代器会在这里停止,rows.Close() 被调用
        }
    }
}

func processUser(u User) {
    fmt.Printf("处理用户: %s (%s)\n", u.Name, u.Email)
}

注意 break 的行为——它会让迭代器函数 return,从而触发 defer rows.Close()。资源清理非常自然。

迭代器 3:目录递归遍历

package main

import (
    "fmt"
    "iter"
    "os"
    "path/filepath"
)

// WalkDir 递归遍历目录
func WalkDir(root string) iter.Seq2[string, error] {
    return func(yield func(string, error) bool) {
        err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
            if err != nil {
                if !yield("", err) {
                    return filepath.SkipAll
                }
                return nil
            }
            if !yield(path, nil) {
                return filepath.SkipAll
            }
            return nil
        })
        if err != nil && err != filepath.SkipAll {
            yield("", err)
        }
    }
}

func main() {
    // 找出所有 .go 文件
    goFiles := 0
    for path, err := range WalkDir(".") {
        if err != nil {
            fmt.Println("错误:", err)
            continue
        }
        if filepath.Ext(path) == ".go" {
            goFiles++
            fmt.Println(path)
        }
    }
    fmt.Printf("共 %d 个 Go 文件\n", goFiles)
}

迭代器 4:分页 API

package main

import (
    "encoding/json"
    "fmt"
    "iter"
    "net/http"
)

// Page 分页响应
type Page[T any] struct {
    Items    []T    `json:"items"`
    NextPage string `json:"next_page"`
}

// Paginate 分页迭代器
func Paginate[T any](firstURL string) iter.Seq2[T, error] {
    return func(yield func(T, error) bool) {
        url := firstURL
        for url != "" {
            resp, err := http.Get(url)
            if err != nil {
                yield(*new(T), err)
                return
            }

            var page Page[T]
            if err := json.NewDecoder(resp.Body).Decode(&page); err != nil {
                resp.Body.Close()
                yield(*new(T), err)
                return
            }
            resp.Body.Close()

            for _, item := range page.Items {
                if !yield(item, nil) {
                    return
                }
            }

            url = page.NextPage
        }
    }
}

type Post struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
}

func main() {
    // 自动翻页,直到 break
    count := 0
    for post, err := range Paginate[Post]("https://api.example.com/posts?page=1") {
        if err != nil {
            fmt.Println("API 错误:", err)
            break
        }
        fmt.Printf("#%d: %s\n", post.ID, post.Title)
        count++
        if count >= 50 {
            break // 只看前 50 条
        }
    }
}

迭代器组合:函数式编程的快感

迭代器最强大的地方在于组合。你可以用小迭代器拼出大迭代器,就像 Unix 管道一样。

Filter:过滤

package main

import "iter"

// Filter 过滤迭代器中的元素
func Filter[V any](seq iter.Seq[V], pred func(V) bool) iter.Seq[V] {
    return func(yield func(V) bool) {
        for v := range seq {
            if pred(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}

func main() {
    // 只取偶数
    evens := Filter(Count(1), func(n int) bool {
        return n%2 == 0
    })

    for v := range evens {
        fmt.Println(v) // 2, 4, 6, 8, 10...
        if v >= 10 {
            break
        }
    }
}

Map:转换

// Map 转换迭代器中的元素
func Map[In, Out any](seq iter.Seq[In], transform func(In) Out) iter.Seq[Out] {
    return func(yield func(Out) bool) {
        for v := range seq {
            if !yield(transform(v)) {
                return
            }
        }
    }
}

func main() {
    // 取前 5 个自然数的平方
    squares := Map(Count(1), func(n int) int {
        return n * n
    })

    count := 0
    for v := range squares {
        fmt.Println(v) // 1, 4, 9, 16, 25
        count++
        if count >= 5 {
            break
        }
    }
}

Take:取前 N 个

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

// 组合使用
func main() {
    // 取前 5 个偶数的平方
    result := Take(
        Map(
            Filter(Count(1), func(n int) bool { return n%2 == 0 }),
            func(n int) int { return n * n },
        ),
        5,
    )

    for v := range result {
        fmt.Println(v) // 4, 16, 36, 64, 100
    }
}

看到这种组合的优雅了吗?每个操作都是惰性的——只有在 for range 循环真正请求数据时,才会产生计算。不会有多余的内存分配,不会有 goroutine 开销。

收集结果:iter 包的辅助函数

iter 包提供了一些辅助函数来收集迭代结果:

package main

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

func main() {
    // slices.Collect 把 Seq[V] 收集成切片
    nums := slices.Collect(Take(Count(1), 5))
    fmt.Println(nums) // [1 2 3 4 5]

    // maps.Collect 把 Seq2[K, V] 收集成 map
    pairs := maps.Collect(Pairs(map[string]int{"a": 1, "b": 2}))
    fmt.Println(pairs) // map[a:1 b:2]

    // slices.AppendSeq 把 Seq 追加到现有切片
    existing := []int{0}
    result := slices.AppendSeq(existing, Take(Count(1), 3))
    fmt.Println(result) // [0 1 2 3]
}

⚠️ 注意Collect 会消耗迭代器,把所有结果放进内存。对无限序列用 Collect 会让程序崩溃。

迭代器 vs Channel:性能对比

很多人会问:迭代器和 channel 到底哪个更好?来看一个基准测试:

package main

import (
    "iter"
    "testing"
)

// Channel 版本
func channelCount(n int) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)
        for i := 0; i < n; i++ {
            ch <- i
        }
    }()
    return ch
}

// Iterator 版本
func iterCount(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i < n; i++ {
            if !yield(i) {
                return
            }
        }
    }
}

func BenchmarkChannel(b *testing.B) {
    for i := 0; i < b.N; i++ {
        sum := 0
        for v := range channelCount(1000) {
            sum += v
        }
        _ = sum
    }
}

func BenchmarkIterator(b *testing.B) {
    for i := 0; i < b.N; i++ {
        sum := 0
        for v := range iterCount(1000) {
            sum += v
        }
        _ = sum
    }
}

运行结果:

BenchmarkChannel-8      100000    11243 ns/op    64 B/op    1 allocs/op
BenchmarkIterator-8    1000000     1124 ns/op     0 B/op    0 allocs/op

迭代器比 channel 快 10 倍,且零内存分配。

原因很简单:

  • channel 需要启动真正的 goroutine,涉及栈分配和调度
  • channel 的每次发送/接收都需要锁和同步
  • 迭代器是同步的控制流切换,编译器直接生成跳转指令,没有任何并发开销

迭代器的坑和注意事项

迭代器虽好,但不是万能的。有几个坑你需要知道:

1. 不要在迭代器里偷偷跑 goroutine

// ❌ 错误:在 yield 期间持有 goroutine
func BadIterator() iter.Seq[int] {
    return func(yield func(int) bool) {
        ch := make(chan int)
        go func() {
            for i := 0; ; i++ {
                ch <- i
            }
        }()
        for v := range ch {
            if !yield(v) {
                return // goroutine 泄漏了!
            }
        }
    }
}

如果调用者 break 了,goroutine 还在 ch <- i 那里阻塞着,永远不会退出。

2. 不要在 yield 中 panic

// ❌ 错误:yield 中 panic 会导致难以调试的问题
func BadIterator2() iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i < 10; i++ {
            yield(i) // 如果调用者在循环体里 panic,堆栈会很奇怪
        }
    }
}

3. 迭代器只能消费一次

fib := Fibonacci()

for v := range fib {
    fmt.Println(v)
    if v >= 10 { break }
}

// ❌ 再次消费 fib 会从上次停止的地方继续(或者已经结束了)
for v := range fib {
    fmt.Println(v) // 可能什么都不会打印
}

如果需要多次遍历,每次都应该重新创建迭代器:

for v := range Fibonacci() { ... }
for v := range Fibonacci() { ... }

标准库中的迭代器

Go 1.23 之后,标准库的一些包开始提供迭代器接口:

// maps.All 返回 map 的迭代器
for k, v := range maps.All(myMap) {
    fmt.Println(k, v)
}

// slices.All 返回切片的迭代器
for i, v := range slices.All(mySlice) {
    fmt.Println(i, v)
}

// slices.Backward 反向遍历切片
for i, v := range slices.Backward(mySlice) {
    fmt.Println(i, v)
}

这些函数返回的都是 iter.Seqiter.Seq2,可以直接用 for range 遍历。

小结

Go 1.23 的迭代器是一个革命性的特性,它让 Go 终于拥有了现代化的数据遍历能力:

核心概念:

  1. range over functionfor v := range func(yield func(V) bool) { ... }
  2. iter.Seq[V]:单值迭代器
  3. iter.Seq2[K, V]:双值迭代器
  4. 惰性求值:数据按需产生,不浪费内存

关键优势:

  • 比 channel 快 10 倍,零内存分配
  • 支持无限序列
  • 可以优雅地组合(Filter、Map、Take)
  • 资源清理通过 defer 自然完成
  • 调用者 break 时迭代器自动停止

何时使用迭代器:

  • 遍历大数据集(文件、数据库、API 分页)
  • 生成无限序列(斐波那契、ID 生成器)
  • 需要惰性求值的算法
  • 替代传统的回调函数模式

何时继续用 channel:

  • 真正需要并发生产者-消费者模式
  • 需要多个 goroutine 同时写入
  • 需要缓冲和背压控制

迭代器不是 channel 的替代品,而是补充。两者各有适用场景,理解它们的区别是写出高性能 Go 代码的关键。

练习时间

  1. 树遍历:为二叉搜索树写一个中序遍历的迭代器
  2. CSV 解析:实现一个 CSV 文件的逐行解析迭代器
  3. 合并迭代器:实现 Merge(seq1, seq2 iter.Seq[int]) iter.Seq[int],交替产出两个序列的元素
  4. 性能测试:对比迭代器、channel、切片三种方式处理 100 万条记录的性能
  5. 函数式工具库:实现 ReduceFlatMapGroupBy 等函数式迭代器工具

下一篇预告

下一篇文章,我们将深入 Go 的测试进阶——从单元测试到基准测试,从表驱动测试到模糊测试。测试不是可选项,它是写出高质量代码的基础设施。我们会学习如何用 Go 的标准测试工具构建完整的测试体系。

我们下篇见!👋


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页