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

Go iter.Seq 怎样设计可组合的惰性遍历

来源:17golang原创

时间:2026-10-20 05:07:45 377浏览 收藏

Go iter.Seq 的惰性组合,关键不在把所有循环改成函数,而是统一用 iter.Seq[T] 连接生成器与组合器:构造时只保存输入和规则,消费时才逐项处理;任何一层收到 yield 返回 false,都必须停止。这样才能在筛选、转换之后仍保留提前退出的能力。

Go 官方地址:https://go.dev/;iter 文档:https://pkg.go.dev/iter。下文采用 Go 1.23 及以上的迭代器语法,项目工具链和语言版本需相应支持,参见 Go 1.23 发布说明。

我会先判断是否真的需要惰性组合

假设要从名称列表里过滤空值、转为大写,但调用方可能只要第一个匹配项。对我来说,这类需求值得把“怎么产生元素”和“取多少元素”分开:调用方掌握退出位置,筛选和转换只承担单项规则。若只是一个很短、只用一次的循环,我仍倾向直接写 for,不为抽象而增加三个函数。

下面是为说明取舍设计的独立场景,不是生产压测或亲历故障记录。这里的预算是阅读成本、额外存储和接口复杂度,而不是云资源价格;也不需要引入 goroutine、channel 或后台任务。

方案我会选择它的条件代价
直接循环规则局部且只使用一次复用时可能重复业务判断
中间切片需要索引、长度或多次访问结果保存每一层结果
Seq 组合多个消费者共享变换规则,且允许早停必须说明停止与重复遍历语义

把生成、筛选与转换留在闭包内

我会将函数划成 FromSlice、Where、Select 三个边界。FromSlice 接收 names 切片;Where 持有 keep 谓词;Select 持有 convert 转换函数。三个函数返回的都是函数值,调用方得到 labels 后才开始 range。以下完整代码为原创示例,未作为实测记录执行。

package main

import (
    "fmt"
    "iter"
    "strings"
)

// FromSlice 只捕获切片;遍历开始后才逐项交付,不复制底层数组。
func FromSlice[T any](items []T) iter.Seq[T] {
    return func(yield func(T) bool) {
        for _, item := range items {
            if !yield(item) {
                return // 消费者不再接收时,立即结束这一层。
            }
        }
    }
}

// Where 仅为满足条件的元素调用下游,不创建中间结果切片。
func Where[T any](src iter.Seq[T], keep func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for item := range src {
            if keep(item) && !yield(item) {
                return // 退出上游 range,使停止意愿继续传递。
            }
        }
    }
}

// Select 将单项转换延迟到消费时;转换函数应尽量没有副作用。
func Select[A, B any](src iter.Seq[A], convert func(A) B) iter.Seq[B] {
    return func(yield func(B) bool) {
        for item := range src {
            if !yield(convert(item)) {
                return
            }
        }
    }
}

func main() {
    names := []string{"spruce", "", "moss", "fern"}
    chosen := Where(FromSlice(names), func(s string) bool {
        return s != "" // 忽略空名称,不把筛选提前执行。
    })
    labels := Select(chosen, strings.ToUpper)

    // 只取首个匹配项;构造 labels 本身不会调用转换函数。
    for label := range labels {
        fmt.Println(label)
        break
    }
}

按这个示例的规则,首个交付值应为 SPRUCE;空名称会被过滤,break 后不再向后转换 moss 与 fern。构造 chosen 与 labels 时不预遍历源,也不先生成一份过滤切片。这个“懒”来自函数实现位置,不是给任意函数加上 Seq 返回类型就自动成立。

生成器和组合器的参数依赖结构
图1:原创静态结构图。生成边界持有 names,组合边界持有筛选与转换规则;线条标明函数参数依赖,不是执行流程图。

提前停止是每一层的共同约定

iter.Seq 是接收 yield 回调的函数类型,yield 的布尔值表示是否继续。官方接口约定见 iter 文档。对 range 函数的语言规则,见 Go 规范的 range 条款:消费者退出循环后,迭代器不能继续调用已返回 false 的 yield。

在这份实现中,Select 与 Where 的 return 会结束各自的上游 range,FromSlice 则直接检查 yield。我的取舍是让所有组合器使用同一种退出写法,而不是某层用 break 跳出内部小循环后又继续交付。停止不是异常,也不需要补发零值或关闭一个并不存在的 channel。

消费者的 break、Select、Where、FromSlice 与 yield 返回值构成同一个停止契约。Select 不得吞掉 false,Where 不能因为还有未检查的元素而继续,FromSlice 不应为“把源跑完”继续调用回调。

各层迭代器共享的停止契约
图2:原创静态契约图。分组区分消费侧与迭代器侧,各层都受 yield 返回值约束;不展示时间线、运行轨迹或测量结果。

复用与物化要在接口上说明

FromSlice 的循环变量位于返回的函数内部,因此对同一函数值再次遍历,会从切片开头重新走。但它只复制了切片描述信息,并没有冻结底层元素;我不会把它命名成 Snapshot。调用方若改了底层数组,后续遍历可能读到变化。Where 与 Select 是否可重复消费,也依赖它们的源以及回调自身的状态。

如果要保存转换结果,可以显式收集。下面片段放在 main 的 labels 构造之后,另需导入标准库 slices;它表示完整消费,而不是继续上一次 break 的位置。

// 物化所有结果:调用方开始承担结果切片的存储。
all := slices.Collect(labels)
// 对本例可重入源再次遍历;不会自动缓存前一次转换结果。
again := slices.Collect(labels)
_, _ = all, again

收集接口的定义见 slices.Collect 文档。我会把收集留在业务边缘:需要索引和保存的调用方才收集,只要逐项处理的调用方继续使用 range。若源是不能回退的流,就必须明确声明一次性语义,不把本例的重入能力推广给所有 Seq。

适合拆成 Seq 的场景与取舍

适合的是规则需要重组、调用方可能早停、元素可逐项独立处理的任务。不适合的是调用方始终要随机访问所有结果,或简单循环比组合关系更容易读懂的地方。本文没有性能数据,不声称 Seq 一定更快或零分配;不建中间切片也不等于没有闭包成本。

Where 和 Select 是标准库自带的吗?

不是,它们是本文定义的通用函数。名称表达角色,接口保持标准 iter.Seq,方便别的生成器接入。

惰性是不是自动异步执行?

不是。本例没有创建后台任务;处理发生在消费者调用迭代器期间。慢回调仍会让本次消费变慢。

回调可以修改外部计数吗?

可以写出这样的代码,但我更愿意让转换与谓词尽量保持纯粹,否则一次早停与一次完整收集可能造成不同的外部状态。提交接口前应写明源是否重入、是否共享底层数据、回调是否有副作用,以及停止后还会不会继续工作。

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