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

Go iter.Seq 怎样组合过滤迭代器

来源:17golang原创

时间:2026-10-14 19:45:02 268浏览 收藏

我第一次把一段“遍历切片、判断条件、追加结果”的代码改成 iter.Seq,最容易写错的地方不是泛型,而是 yield 的返回值:下游返回 false 时,过滤器必须立刻返回,不能继续向上游取值。这样封装出来的过滤器才既能组合,又能保留惰性和提前停止。

官方文档:https://pkg.go.dev/iter

下面的示例以 Go 1.23 及更高版本为范围。Go 1.23 让 for range 可以遍历符合约定的函数类型,并新增 iter.Seq、slices.Collect 等配套能力。本文只围绕 Seq[V] 的过滤和停止传播展开,不把它扩展成并发流水线。

从旧循环迁到 iter.Seq

旧写法通常把筛选逻辑和消费逻辑揉在一起:循环拿到一个值,判断后追加到结果切片。它当然没有问题,但当输入从切片换成映射值、文件行或另一个生成器时,过滤代码很容易复制多份。

func LongNames(names []string) []string {
	result := make([]string, 0, len(names))
	for _, name := range names {
		// 只保留长度达到条件的名称。
		if len(name) >= 6 {
			result = append(result, name)
		}
	}
	return result
}

迁移后的思路是把“值从哪里来”和“值是否保留”拆开。输入统一为 iter.Seq[V],过滤器只负责调用谓词和转发 yield,最后是否收集成切片由调用方决定。

关注点旧循环iter.Seq 组合方式
输入形态通常绑定切片或某一种容器只要求实现 iter.Seq[V]
过滤位置写在消费循环内部独立成可复用适配器
结果分配循环时通常立即追加需要时再用 slices.Collect
停止语义由循环里的 break 控制由 yield 的 false 向上游传播

实现可组合的过滤器

过滤器的签名可以保持很小:接收一个上游序列和一个判断函数,返回另一个序列。真正的迭代发生在返回的闭包被消费时,所以创建过滤器本身不会遍历输入。

package main

import "iter"

// Filter 返回一个惰性过滤器,只把满足 keep 的值交给下游。
func Filter[V any](input iter.Seq[V], keep func(V) bool) iter.Seq[V] {
	return func(yield func(V) bool) {
		for value := range input {
			// 谓词不通过时跳过当前值,但继续观察后续值。
			if !keep(value) {
				continue
			}
			// 下游返回 false 表示已经不需要更多值,必须停止转发。
			if !yield(value) {
				return
			}
		}
	}
}

这里有两个边界值得记住。第一,keep 为假只代表当前值被过滤,不代表整个序列结束。第二,yield 为假才是下游的停止信号;过滤器要用 return 把它传回产生输入的那一层。

Go iter.Seq 从源序列经过 Filter 谓词到下游 range 的结构说明图
图1:iter.Seq 过滤器把上游值交给谓词判断,再把保留值交给下游;这是静态结构说明图,不是运行截图。

叠加条件并收集结果

因为 Filter 的输入和输出都是 iter.Seq[V],多个条件可以像套壳一样组合。下面先从切片得到序列,再叠加两个过滤条件,最后才把结果收集到新切片。

package main

import (
	"fmt"
	"slices"
)

func main() {
	values := []int{3, 8, 12, 15, 21, 24}
	positive := slices.Values(values)

	// 第一层筛选偶数,第二层筛选大于 10 的值。
	largeEven := Filter(
		Filter(positive, func(value int) bool {
			return value%2 == 0
		}),
		func(value int) bool {
			return value > 10
		},
	)

	// 只有需要物化结果时才分配并返回切片。
	result := slices.Collect(largeEven)
	fmt.Println(result)
}

两层过滤器不会因为嵌套就提前生成中间切片。消费端通过 range 取值时,外层过滤器向内层要值,内层再向 slices.Values 要值;每个值只沿着当前组合链路向前传递。若结果只需要找到第一个匹配项,就不必调用 slices.Collect。

让提前停止向上游传播

我觉得这是从普通循环迁移时最容易漏掉的回归点。下游一旦找到答案,yield 会返回 false。这个返回值会让过滤器的 yield(value) 返回假,于是过滤器执行 return,上游的 range input 也随之结束。

// FirstMatch 返回第一个满足条件的值,不会继续扫描剩余输入。
func FirstMatch[V any](input iter.Seq[V], keep func(V) bool) (V, bool) {
	var found V
	for value := range Filter(input, keep) {
		// 找到目标后离开 range,编译器会让迭代器收到停止信号。
		found = value
		return found, true
	}
	return found, false
}

不要把过滤器写成“无论下游要不要,都继续把全部值推完”的版本。那样在文件行、分页结果或带清理动作的生成器上,会把本来可以提前结束的工作继续做完。

Go iter.Seq 下游 yield 返回 false 向上游传播提前停止的结构说明图
图2:下游不再需要值时,false 沿 yield 边界让过滤适配器返回;这是静态关系说明图,不是运行截图。

如果过滤器包裹的上游还持有文件、锁或其他需要释放的资源,资源的生命周期应由上游迭代器自己管理,并在收到停止后完成清理。过滤器的职责是准确转发停止信号,不要在中间层偷偷吞掉这个边界。

迁移后的回归检查

把已有循环改为 iter.Seq 后,我会按下面的顺序做一轮小检查。它们不是额外的框架要求,而是用来确认迁移没有改变原来的结果和停止语义。

  • 编译环境至少是 Go 1.23;旧工具链不能解析 range-over-function 的写法。
  • 谓词返回假时只跳过当前值,不能把它误写成终止整个序列。
  • 每次调用下游 yield 都检查布尔返回值,并在假值分支立刻 return。
  • 空输入、全部被过滤和第一个值就命中的情况都要有明确结果。
  • 只有调用方确实需要随机访问或重复遍历时,才把序列收集成切片。
场景期望行为
输入为空过滤器不调用谓词,消费者直接结束
没有值通过返回空序列,收集结果为空切片
消费者提前返回过滤器停止调用下游,上游不再请求后续值
多个 Filter 嵌套每层只处理自己收到的值,并保留 false 传播

什么时候保留普通循环

iter.Seq 适合把同一套筛选逻辑应用到不同来源,或者让调用方决定“遍历到哪里”。但如果逻辑只有几行、只使用一次,而且结果本来就必须是切片,普通循环通常更直观。迁移的收益来自复用和惰性,不是把所有循环都包成函数。

我的判断是:先把谓词和停止边界写正确,再决定是否抽成通用适配器。只要记住 yield(false) 是“下游不再需要”的信号,Filter 就能自然地叠加,也不会在找到结果后继续消耗上游。

常见追问

iter.Seq 和切片有什么区别?切片是已经物化的数据,支持索引;iter.Seq 是按需产生值的函数约定,本身不提供索引。

过滤器是否必须使用泛型?为了让同一实现适配不同元素类型,泛型最自然;如果业务只有一种类型,也可以写具体类型版本。

什么时候应该调用 slices.Collect?当调用方需要保存结果、排序、索引或多次遍历时再收集;只需要顺序消费或找到首个结果时可以保持惰性。

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