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.Seq 或 iter.Seq2,可以直接用 for range 遍历。
小结
Go 1.23 的迭代器是一个革命性的特性,它让 Go 终于拥有了现代化的数据遍历能力:
核心概念:
- range over function:
for v := range func(yield func(V) bool) { ... } - iter.Seq[V]:单值迭代器
- iter.Seq2[K, V]:双值迭代器
- 惰性求值:数据按需产生,不浪费内存
关键优势:
- 比 channel 快 10 倍,零内存分配
- 支持无限序列
- 可以优雅地组合(Filter、Map、Take)
- 资源清理通过
defer自然完成 - 调用者
break时迭代器自动停止
何时使用迭代器:
- 遍历大数据集(文件、数据库、API 分页)
- 生成无限序列(斐波那契、ID 生成器)
- 需要惰性求值的算法
- 替代传统的回调函数模式
何时继续用 channel:
- 真正需要并发生产者-消费者模式
- 需要多个 goroutine 同时写入
- 需要缓冲和背压控制
迭代器不是 channel 的替代品,而是补充。两者各有适用场景,理解它们的区别是写出高性能 Go 代码的关键。
练习时间
- 树遍历:为二叉搜索树写一个中序遍历的迭代器
- CSV 解析:实现一个 CSV 文件的逐行解析迭代器
- 合并迭代器:实现
Merge(seq1, seq2 iter.Seq[int]) iter.Seq[int],交替产出两个序列的元素 - 性能测试:对比迭代器、channel、切片三种方式处理 100 万条记录的性能
- 函数式工具库:实现
Reduce、FlatMap、GroupBy等函数式迭代器工具
下一篇预告
下一篇文章,我们将深入 Go 的测试进阶——从单元测试到基准测试,从表驱动测试到模糊测试。测试不是可选项,它是写出高质量代码的基础设施。我们会学习如何用 Go 的标准测试工具构建完整的测试体系。
我们下篇见!👋
参考资料:
- Go 1.23 Release Notes - Range over Function
- iter 包文档
- Go Blog - Range Over Function Types
- Go 提案 - Range over function
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。