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

iter.Seq 连接多个数据源时的停止协议

来源:17golang原创

时间:2026-10-10 13:08:14 279浏览 收藏

把多个 iter.Seq[V] 连接成一条序列,最稳妥的默认方案是顺序串联:当前数据源完全结束后再进入下一个;一旦下游的 yield 返回 false,组合器立刻返回,不再打开后续数据源。停止协议只有一条主线——消费者表达“不再需要”,组合器负责传播,数据源负责清理自己的资源。

Go 官方对 iter.Seq 的定义正是 func(yield func(V) bool)。序列向 yield 交付值;返回 true 表示继续,返回 false 表示停止。连接多个源时,不能吞掉这个布尔值,也不能把空数据源误判成全局停止。

顺序 Concat 适合什么场景

顺序串联适合“来源有先后优先级、元素类型一致、无需并发交错”的任务。例如先读内存缓存,再读本地文件,最后读取远端分页;或者把多个分片按固定顺序暴露给同一个消费者。它保留惰性:消费者没有走到某个源,该源就不需要创建连接或分配缓冲区。

如果目标是同时等待多个实时流,顺序 Concat 并不合适。那是并发扇入问题,需要额外定义顺序、取消、错误和背压,而不仅是把几个 Seq 放进切片。

内存Seq、文件Seq和网络Seq通过Concat泛型组合器连接到消费者的模块边界说明图
图1:多个 iter.Seq 与 Concat[V] 的模块边界说明图。

最小写法:嵌套 range 直接传播停止

最小实现只需要两层 range。外层选择数据源,内层读取当前源。关键不是循环本身,而是 yield(v) 返回 false 后必须直接 return,这样后续数据源不会启动。

package sequtil

import "iter"

// Concat 按参数顺序连接多个序列。
func Concat[V any](sources ...iter.Seq[V]) iter.Seq[V] {
    return func(yield func(V) bool) {
        for _, source := range sources {
            // 当前源为空时自然进入下一个源,不代表全局停止。
            for value := range source {
                if !yield(value) {
                    // 下游不再需要数据,后续源也不能启动。
                    return
                }
            }
        }
    }
}

这段写法不需要额外的 done 通道。消费者在 for value := range combined 中执行 break 时,编译器生成的 yield 会向组合器返回 false;组合器随即返回,当前源也会收到停止信号并退出。这个传播链要求每一层都尊重 false。

停止信号只认 yield 的返回值

一个常见错误是调用 yield 后忽略结果。代码看似还能输出元素,但下游提前退出后,上游会继续读取文件、翻页或发送网络请求。另一个错误是当前源结束时直接返回组合器,这会让第一个空源截断整个序列。

// BadConcat 展示两个容易混淆的错误,不能用于生产代码。
func BadConcat[V any](sources ...iter.Seq[V]) iter.Seq[V] {
    return func(yield func(V) bool) {
        for _, source := range sources {
            for value := range source {
                // 错误:忽略 false 会破坏提前停止协议。
                yield(value)
            }
            // 错误:一个源结束不等于所有源都结束。
            return
        }
    }
}

Seq 本身没有独立的 stop 方法。对于推送式迭代器,yield(false) 就是停止接口。只有把 Seq 转换成拉取式迭代器后,才会得到显式的 stop 函数。

消费者、yield false、Concat、当前Seq和资源清理函数之间停止责任关系的说明图
图2:提前停止信号与资源所有权的静态关系说明图。

资源释放应该写在每个数据源内部

Concat 不知道源里持有什么资源,因此不应替文件源关闭网络响应,也不应猜测某个 goroutine 的终止方式。资源型序列应在自身调用边界内完成申请与释放。只要组合器正确传播 false,源函数返回时的 defer 就会执行。

package main

import (
    "bufio"
    "iter"
    "os"
)

// FileLines 惰性读取文本行,打开与关闭都由源自身负责。
func FileLines(path string) iter.Seq[string] {
    return func(yield func(string) bool) {
        file, err := os.Open(path)
        if err != nil {
            // 本例聚焦停止协议;真实项目应使用 Seq2 传递错误。
            return
        }
        defer file.Close()

        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            if !yield(scanner.Text()) {
                // 消费者提前停止,返回后立即执行文件关闭。
                return
            }
        }
    }
}

上例为了保持 Seq[string] 简洁,省略了错误通道。需要区分打开失败、扫描失败和正常结束时,可改为 iter.Seq2[string, error],并在错误出现后停止继续产出;不要把错误悄悄当成空序列。

什么时候改用 iter.Pull

iter.Pull 把推送式 Seq 转成 next 与 stop。它适合需要交替读取两个序列、比较相邻来源、做归并排序或由调用方主动决定下一次从哪个源取值的场景。代价是停止责任变得显式:只要 next 还没有返回 false,调用方不再读取时就必须调用 stop。官方文档建议通常直接 defer stop()。

// FirstFromEach 从每个源最多取一个值,展示 Pull 的清理要求。
func FirstFromEach[V any](sources ...iter.Seq[V]) []V {
    result := make([]V, 0, len(sources))

    for _, source := range sources {
        next, stop := iter.Pull(source)
        value, ok := next()
        if ok {
            result = append(result, value)
        }
        // 当前循环立即结束拉取,不能把 stop 延迟到整个函数末尾。
        stop()
    }
    return result
}

如果在循环里写 defer stop(),所有停止动作会堆到函数返回时才执行,资源占用时间可能明显变长。可以像上例一样在本轮结束时直接调用,或者把单次拉取封装进小函数并在小函数中使用 defer。

三种连接方式怎么选

方案适用条件停止方式主要代价
顺序 Concat固定顺序、惰性读取逐层传播 yield(false)不能并行交错多个源
iter.Pull交替读取、归并、对齐比较未读完时调用 stop调用方承担显式清理
先收集再拼接数据量小、需要随机访问收集完成后无迭代停止问题失去惰性并占用额外内存

默认优先顺序 Concat。只有当消费者必须控制“下一次读哪个源”时,再引入 Pull;只有当数据规模小且后续确实需要索引、排序或重复遍历时,才先收集到切片。并发实时流则应单独设计取消和背压协议,不要把它伪装成普通 Concat。

验收停止协议的四个检查点

  • 消费者在第一个源中途 break 后,当前源的清理函数会执行。
  • 提前停止后,第二个及后续数据源没有被打开或调用。
  • 某个空源只会被跳过,不会阻止后续非空源产出。
  • 使用 iter.Pull 且未读到结尾时,所有路径都会调用 stop。

几个容易继续追问的问题

Concat 能否忽略 nil Seq? nil 函数被调用会 panic。若业务允许 nil,组合器应显式跳过;更推荐在构造源列表时消除 nil,让类型边界更清楚。

源发生 panic 怎么处理? Seq 不会自动吞掉 panic。资源型源的 defer 仍会按 Go 规则执行,是否恢复应由更外层的故障边界决定。

多个源元素类型不同怎么办? 先映射成统一领域类型,再传给 Concat;不要为方便而把所有值降成 any,否则停止之外还会引入运行时类型判断。

官方资料在哪里? 可参考 Go iter 包文档和 Go Blog:Range Over Function Types。

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