登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go iter.Seq 怎样把回调式数据暴露为迭代器

来源:17golang原创

时间:2026-11-01 17:55:11 407浏览 收藏

把同步回调式数据源暴露为 iter.Seq[T],最小做法是返回一个接收 yield func(T) bool 的函数,再把 yield 传给原来的访问函数。前提是原接口同步调用回调,并在回调返回 false 后停止。这样调用方就能使用 for value := range seq,并用 break 提前退出。

官方地址:https://pkg.go.dev/iter

Go 1.23 引入了函数迭代的语言支持和 iter 标准库包。这里说明的是这项能力如何接入已有回调 API,不把它当作最新版本发布消息。示例要求工具链及当前包的语言版本支持 Go 1.23 或更高版本;相关历史变更见Go 1.23 发布说明。

用一层函数把同步回调适配为 Seq

先约定一个可停止的数据源类型 Visit[T]。它不返回错误,也不保存回调,不并发调用回调。下面完整程序将三个名字按顺序交给调用者,在第二个名字处结束。

package main

import (
    "fmt"
    "iter"
)

// Visit 必须同步调用回调;收到 false 后停止,返回后不再调用回调。
type Visit[T any] func(func(T) bool)

func AsSeq[T any](visit Visit[T]) iter.Seq[T] {
    return func(yield func(T) bool) {
        visit(yield) // 原样传递继续或停止信号,不创建后台任务。
    }
}

func main() {
    visit := Visit[string](func(emit func(string) bool) {
        for _, name := range []string{"alpha", "bravo", "charlie"} {
            if !emit(name) {
                return // 消费者不再需要数据,源端立即结束。
            }
        }
    })

    for name := range AsSeq(visit) {
        fmt.Println(name)
        if name == "bravo" {
            break // 停止信号会通过 yield 传回 visit。
        }
    }
}

适配器的核心只有 visit(yield)。这里没有中间切片,也没有新建 goroutine 或 channel。返回的 Seq 是函数值;按照这个实现,调用 AsSeq 只是建立闭包,开始 range 时才执行 visit。

本例不需要为了类型名称改写所有旧代码。已有的命名函数类型若底层签名一致,可以按 Go 的类型规则显式转换成 Visit;签名不同则写一层真正的参数适配。不要仅靠类型断言把任意回调接口变成可停止接口。

同步访问源、适配器与消费者之间的静态调用结构
图1:静态结构图。数据源组中的Visit与回调值,通过适配层的AsSeq和yield关联消费者组的range与停止信号;连线只表达接口关系,不是执行步骤。

这项能力改变了调用方式,没有改变数据源

对调用者而言,变化是把处理逻辑放回普通循环体:筛选使用 continue,找到目标使用 break。对提供者而言,Visit 仍然负责决定元素顺序和何时停止。iter.Seq 并不是自动缓存、自动重试或自动并行的数据容器。

可把这个关系理解成三个角色:数据源负责取值,适配器接通回调,消费者通过循环处理值。yield 的布尔结果是停止信号:true 表示可以继续,false 表示消费者结束了这次遍历。生产者收到 false 后不能再次调用 yield。

原接口能力能否直接适配需要保留的约定
同步 func(func(T) bool)可以使用 AsSeqfalse 后停止,返回后不回调
同步 func(func(T) bool) error需设计错误出口错误与主动停止分开表达
同步 func(func(T))不能照搬最小写法源端没有停止通道
订阅式异步事件不属于本文适配器必须另外管理取消、等待和资源关闭

不要在消费者 break 后捕获并吞掉由错误迭代器触发的 panic,再让生产者继续运行。这会掩盖契约问题。应修正源端停止逻辑。函数 range 的具体要求见语言规范的 range 条款。

不能停止的回调需要先改契约

如果旧接口的回调没有返回值,源端通常不知道消费者已经退出。外层可以设置 stopped 标记,从此不再调用 yield,但这只能避免继续向消费者发送,不能让底层停止读盘、翻页或计算。一个仍要跑完整批数据的适配器,不应宣传为“break 即停止源任务”。

较直接的迁移是新增一个可停止入口,例如保留旧的遍历方法,同时增加接收布尔回调的新方法。旧入口可调用新入口并始终返回 true,旧调用方的行为保持一致;新调用方使用 Seq。若第三方 API 已经提供专用停止错误,也可以按其文档把 false 映射成该停止方式,但不能猜测错误值或把所有错误都当作主动退出。

异步订阅也不能简单地把 yield 塞给后台事件处理器后立即返回。那样迭代函数可能已经结束,回调仍在执行。本文的 Visit 契约明确要求:回调在这次 visit 调用的有效期间同步发生,回调不被保留,不会被多个 goroutine 同时使用。需要异步桥接时,应另外设计有界缓冲、取消协议、等待和错误通道;这些能力不是 Seq 类型名称自带的。

有错误的数据源可以提供 Seq2

Seq 的函数签名没有单独的最终错误返回值。可以选择结果结构体、显式状态对象或成对序列。这里采用 iter.Seq2[T, error],并在项目 API 中约定成功项是值加 nil,源失败时再发送一个零值加错误的终止项。这是本文的错误协议,Seq2 本身并不强制第二个值代表错误。

// 以下定义放在同一 package 中,沿用 iter 导入。
type VisitErr[T any] func(func(T) bool) error

// AsSeq2 的项目约定:成功项为 (值,nil),末尾错误项为 (零值,错误)。
func AsSeq2[T any](visit VisitErr[T]) iter.Seq2[T, error] {
    return func(yield func(T, error) bool) {
        stopped := false // 每次遍历独立保存停止状态。
        err := visit(func(v T) bool {
            if stopped {
                return false // 防止源误调用时再次进入已停止的 yield。
            }
            if !yield(v, nil) {
                stopped = true
                return false
            }
            return true
        })
        if stopped {
            return // break 后不能再发送末尾错误项。
        }
        if err != nil {
            var zero T
            yield(zero, err) // 最后一次发送后直接返回,不再产出。
        }
    }
}

stopped 放在迭代函数内部,每次调用都重新建立。它有两个作用:消费者停止后不再调用 yield;visit 返回后即使带有错误,也不再试图向已经结束的循环补发错误项。这个保护并不能取消一个拒绝停止的源,也不使不合约的源自动变得正确。

// 接在 main 中演示错误源,AsSeq2 的约定由当前项目定义。
source := VisitErr[int](func(emit func(int) bool) error {
    for _, n := range []int{12, 24} {
        if !emit(n) {
            return nil // 主动停止不是源读取故障。
        }
    }
    return fmt.Errorf("示例数据源读取失败")
})
for value, err := range AsSeq2(source) {
    if err != nil {
        fmt.Println("读取错误:", err)
        break // 错误项中的 value 是零值,不参与业务处理。
    }
    fmt.Println("值:", value)
}

消费者必须先判断 err,再使用 value。整数零值也可能是有效业务数据,所以不能以 value == 0 判断失败。这个例子只演示协议,没有执行真实网络或文件访问。

成功项、停止状态与终止错误项的静态协议关系
图2:静态结构图。源接口组中的VisitErr和源错误,与适配状态组中的stopped、错误协议关联;输出对象组的成功项、终止错误项对应两类数据。图示不是运行结果。

提前停止与源失败还存在一个重要取舍:循环已 break 时,再产生的清理错误不会通过这条序列送到调用者。若关闭失败必须影响最终任务状态,就不能只依靠终止错误项;应增加独立的 Close/Err 接口或保留返回 error 的驱动方法,并明确调用方需要读取最终状态。不要把 Seq2 包装成“所有错误都一定可见”。

迁移时明确资源、重复遍历和取消约定

优先保留原来的数据源方法,再增加返回 Seq 的方法,让调用方逐步迁移。这一层封装不应同时更改元素顺序、数据快照规则和错误分类,否则很难判断行为差异来自哪里。对较低语言版本的消费者,可以继续提供原回调接口,不要求所有调用方立即升级。

资源归属至少要回答:资源是在构造序列时还是开始遍历时取得;正常结束、break 和错误分别由谁释放;序列从未被遍历时,是否已经占有句柄。如果资源由每次遍历创建,通常应把取得和 defer 清理写在迭代函数内。若资源属于调用者,就不能擅自在适配器里关闭它。

还应写明是否允许重复遍历。最小示例每次都重新枚举固定名字,因此可以重复读;闭包若引用一个只能前进的读取器,再次 range 可能从上次位置继续,也可能已经耗尽。把函数包装为 Seq 不会自动将一次性流变成可重放集合。

同样,元素里的切片、指针或其他引用仍可能指向源端复用的存储。若底层数据仅在回调期间有效,消费者需要长期保存时必须按该数据的所有权规则复制。Seq 不自动深拷贝元素。适配层也不自动提供取消:源接口若需要 context,应由双方明确传入并处理它,不要把消费者 break 等同于能中断已经阻塞的外部读取。

用小样例核对适配器的停止行为

接入前最值得核对的是源是否遵守 false,而不是只看 for range 能否编译。下面片段可接在完整示例 main 中,观察只消费前两项时源回调次数是否符合约定。

// 接在 main 中,演示 break 后源是否仍发送第三项。
calls := 0
stoppedVisit := Visit[int](func(emit func(int) bool) {
    for _, n := range []int{7, 14, 21} {
        calls++
        if !emit(n) {
            return
        }
    }
})
for n := range AsSeq(stoppedVisit) {
    if n == 14 {
        break // 按此源的契约,第三次 emit 不应发生。
    }
}
fmt.Println("回调次数:", calls)

按代码逻辑,回调次数应为 2;这是推导的预期,不是本文执行记录。把消费者改为完整遍历时,应得到三次回调;数据源为空时应没有循环体执行。错误版本还应分别观察“完整遍历后收到末尾错误”和“提前 break 后不再调用 yield”这两种情况。本文未运行这些代码或测试。

必须使用 iter.Pull 吗?

不需要。同步的布尔回调已经符合推送式迭代的形态。Pull 用于把标准推送迭代器转成 next/stop 形式,不能补出旧数据源缺少的停止能力。

可以返回 iter.Seq 而不在函数体中启动访问吗?

可以,本文 AsSeq 就是延迟到遍历时访问。但这只说明访问函数的调用时机;构造函数若提前打开了文件,资源依旧已经被取得。

为什么不把错误写进共享变量?

这种方式可以设计,但需明确变量何时重置、何时读取,以及是否允许重复或并发遍历。本文把成功值和末尾错误放进同一次 Seq2 调用,减少共享状态;需要可靠报告清理结果的接口仍应提供独立出口。

最小适配应建立在源接口已经具备的同步、停止和资源约定上。先把 false 正确传回源,再决定错误出口,最后才让调用者切换为 range,能让接口变化保持清楚且可解释。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>