Go 1.23 range-over-func 迭代器实战:自定义可迭代类型、提前退出与惰性求值

Go 1.23 range-over-func 迭代器实战:自定义可迭代类型、提前退出与惰性求值

在 Go 1.23 之前,想让自己的类型支持 for range,你要么把数据全塞进 slice 返回(内存全量拷贝),要么暴露一个 Next() (T, bool) 的手写迭代器(调用方要写一堆样板)。前者对大数据流不友好,后者不能用 for range 语法糖。

Go 1.23 正式落地了 range-over-func:只要你的函数签名符合 iter.Seq[V]iter.Seq2[K,V],就能直接被 for range 遍历。这篇用几个能跑的例子把它讲透,包括最容易踩的「提前 break 后清理没执行」的坑。

先看朴素写法的痛点

假设要遍历一个链表,老写法只能这样:

go 复制代码
type Node struct {
    Val  int
    Next *Node
}

// 方案一:全量收集到 slice,链表很长时白白拷贝一份
func (n *Node) ToSlice() []int {
    var out []int
    for cur := n; cur != nil; cur = cur.Next {
        out = append(out, cur.Val)
    }
    return out
}

for _, v := range list.ToSlice() { ... }   // 调用方无感,但内存翻倍

如果链表有一千万个节点,ToSlice 会实打实分配一千万个 int 的空间。而很多时候你只是想找到第一个满足条件的元素就 break------全量拷贝纯属浪费。

range-over-func:惰性、不拷贝

iter.Seq[V] 的本质是这样一个类型:

go 复制代码
// 标准库 iter 包里的定义
type Seq[V any] func(yield func(V) bool)

你写一个函数,它接收一个 yield 回调;每产出一个值就调用 yield(v),如果 yield 返回 false(调用方 break 了),你就停止。给链表加迭代器:

go 复制代码
import "iter"

func (n *Node) All() iter.Seq[int] {
    return func(yield func(int) bool) {
        for cur := n; cur != nil; cur = cur.Next {
            // yield 返回 false 说明调用方不想要了(break/return),立刻停
            if !yield(cur.Val) {
                return
            }
        }
    }
}

调用方现在可以直接 for range,而且边遍历边产出,不预先拷贝:

go 复制代码
for v := range list.All() {
    if v > 100 {
        break // yield 会返回 false,上面的迭代器函数收到后 return
    }
    fmt.Println(v)
}

break 之后,链表根本不会继续往后走。这就是惰性求值的价值。

带 key 的迭代器:iter.Seq2

需要同时产出下标或键值,用 iter.Seq2[K, V],yield 变成两个参数:

go 复制代码
func Enumerate[V any](s []V) iter.Seq2[int, V] {
    return func(yield func(int, V) bool) {
        for i, v := range s {
            if !yield(i, v) {
                return
            }
        }
    }
}

for i, v := range Enumerate([]string{"a", "b", "c"}) {
    fmt.Printf("%d=%s\n", i, v) // 0=a 1=b 2=c
}

组合迭代器:像流水线一样串起来

迭代器最爽的地方是可组合。写一个通用的 Filter,它吃一个 Seq、吐一个 Seq,中间不产生任何中间 slice:

go 复制代码
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) && !yield(v) {   // 命中条件才往下游 yield
                return
            }
        }
    }
}

// 只要链表里的偶数,全程零中间分配
for v := range Filter(list.All(), func(x int) bool { return x%2 == 0 }) {
    fmt.Println(v)
}

Go 1.23 的标准库 slicesmaps 也补了配套函数:slices.Collect(seq) 把迭代器收成 slice,maps.Keys(m) 直接返回 iter.Seq,slices.Sorted(maps.Keys(m)) 一行拿到排序后的 key。

最容易踩的坑:提前 break 时的资源清理

如果你的迭代器持有需要关闭的资源(文件、数据库游标),千万别只在循环正常结束后 Close------调用方可能 break。正确做法是用 defer:

go 复制代码
func LinesOf(path string) iter.Seq[string] {
    return func(yield func(string) bool) {
        f, err := os.Open(path)
        if err != nil {
            return
        }
        defer f.Close() // 关键:无论 yield 返回 false 提前退出,还是读完,都会关

        sc := bufio.NewScanner(f)
        for sc.Scan() {
            if !yield(sc.Text()) {
                return // defer 在这里生效,文件被关闭
            }
        }
    }
}

defer f.Close() 写在迭代器函数体里,而不是外面。因为 yield 返回 false 时你会 return,defer 保证文件一定被关。这是手写迭代器最容易漏的地方。

另一个坑:yield 的返回值必须尊重

新手常写成产出后不检查 yield 返回值:

go 复制代码
// 错误:忽略 yield 返回值,调用方 break 了你还在硬产出
for _, v := range data {
    yield(v) // 没判断返回值
}

调用方 break 后再调用 yield,运行时会 panic(range function continued iteration after function returned)。永远写成 if !yield(v) { return }

小结

  • Go 1.23 的 range-over-func 让自定义类型直接支持 for range,底层是 iter.Seq[V] = func(yield func(V) bool)
  • 核心优势:惰性求值、零中间拷贝 ,break 会让 yield 返回 false,迭代器立刻停。
  • 迭代器可组合:FilterMap 串成流水线,全程不生成中间 slice;配合 slices.Collect/maps.Keys 更顺手。
  • 两条铁律:持有资源就在迭代器函数体里 defer Close();产出永远写 if !yield(v) { return },否则 panic。
  • 记忆点:yield 返回 false 就是调用方喊停的信号,尊重它,你的迭代器就既高效又安全
相关推荐
对象存储与RustFS1 小时前
给 Kubernetes 找一个对象存储后端:RustFS Helm 部署 + 应用接入实录
后端·云原生·kubernetes
用户8181870627461 小时前
第7章 死锁排查实录:jstack定位死锁的完整流程
java·后端
逻辑帧1 小时前
给 AI 时代找工作的同学一些实用建议
前端·后端
喵同志不止步于码农1 小时前
Java Caffeine 快速入门
java·开发语言·后端·并发·缓存一致性
2kπ2 小时前
QT开发笔记
开发语言·笔记·qt
青 春 记 忆2 小时前
零基础入门Python15|关联、聚合、索引与事务:订单数据库
开发语言·python·后端开发
东风破_2 小时前
NestJS CRUD 实战:用 Todos 模块打通 Controller-Service-Module 三层
后端
东风破_2 小时前
NestJS 框架入门:从工厂模式到模块化架构
后端
江屿风2 小时前
【STM32基础篇】【嵌入式生态问题及历史追溯】流食般投喂
大数据·开发语言·人工智能·笔记·stm32·嵌入式硬件