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

Go iter.Seq把回调序列接入for range的工程方式

来源:17golang原创

时间:2026-09-20 08:42:21 364浏览 收藏

如果一个容器只能通过回调把元素逐个交给调用方,调用代码往往会被嵌套的 yield 和停止标记包住。Go 1.23 提供的 iter.Seq 可以把这类回调序列接成 for range:生产者负责调用 yield,消费者负责遍历;当循环结束或执行 break 时,生产者应停止继续提供数据并完成清理。

官方资料:https://pkg.go.dev/iter

要点速览
  • iter.Seq[V] 的本质是 func(yield func(V) bool),返回 false 就表示不再需要后续值。
  • 把分页、树遍历或流式读取封装为序列后,调用方可以直接使用 for v := range seq
  • 需要手动逐项拉取时用 iter.Pull,提前停止时必须调用返回的 stop

一、先看 iter.Seq 与 yield 的职责分工

iter.Seq[V] 不是保存结果的切片,而是一段“如何产生结果”的函数。它把每个值交给 yield,只有 yield 返回 true 才继续。这个返回值是惰性遍历能安全提前结束的关键。

Go iter.Seq、yield 与 for range 的值传递和提前停止关系说明图
图1:iter.Seq 与 for range 的静态关系说明图,重点查看 yield 返回 false 后的停止边界。
package main

import (
	"fmt"
	"iter"
)

// Values 返回一个惰性序列;只有调用方继续消费时才产生下一个值。
func Values(items []string) iter.Seq[string] {
	return func(yield func(string) bool) {
		for _, item := range items {
			// false 表示调用方已经停止,生产者必须立刻退出。
			if !yield(item) {
				return
			}
		}
	}
}

func main() {
	for value := range Values([]string{"Go", "iter.Seq", "range"}) {
		fmt.Println(value) // 每次只接收当前值,不需要预先构造结果切片。
	}
}

实际项目中应把返回类型写成 iter.Seq[string],让接口意图更清楚。标准命名约定也建议集合的遍历方法使用 All,例如 func (s *Set[V]) All() iter.Seq[V]

二、把回调遍历函数改造成可 range 的序列

改造旧的回调 API 时,不要把所有数据先收集到切片。下面用“按页取数据”的伪接口演示工程结构:每次拿到一页就逐项交给 yield,页为空才代表自然结束。

package feed

import "iter"

// PageReader 代表一次读取一页数据的依赖,next 从游标继续读取。
type PageReader[T any] func(next string) (items []T, nextCursor string, err error)

// AllPages 将分页回调转换为可被 for range 消费的 Seq。
func AllPages[T any](read PageReader[T]) iter.Seq[T] {
	return func(yield func(T) bool) {
		cursor := ""
		for {
			items, next, err := read(cursor)
			if err != nil {
				return // 生产者接口不能把错误塞进 Seq,真实项目应另行传递错误状态。
			}
			for _, item := range items {
				if !yield(item) {
					return // 调用方 break 后,停止请求后续页面。
				}
			}
			if next == "" {
				return // 没有下一页,正常结束。
			}
			cursor = next
		}
	}
}

这个例子有一个重要边界:Seq 的签名没有 error 返回值。若读取错误不能静默结束,应把错误写入受控状态、返回一个带结果状态的对象,或继续保留显式的“值、错误”回调接口,不能让消费者误以为“数据已经读完”。

三、用 break 和 defer 验证提前结束与清理

for range 提前退出时会让迭代器的 yield 返回 false。因此生产者要把连接、文件或临时缓冲区的释放放在自己的 defer 中,而不是指望调用方再猜测释放时机。

Go iter.Seq 提前 break 后触发 stop 与 defer cleanup 的资源清理边界说明图
图2:提前结束与资源清理的静态边界说明图,展示 break 触发停止并回收资源。
func FirstMatch[T any](seq iter.Seq[T], match func(T) bool) (T, bool) {
	var found T
	ok := false
	for value := range seq {
		if match(value) {
			found, ok = value, true
			break // break 让迭代器停止,不再请求后续值。
		}
	}
	return found, ok
}

只要迭代器内部确实在 yield 返回 false 后退出,外层的 break 就不会继续触发读取。对于网络流或文件流,仍建议在迭代器内部写出清理路径并记录必要的错误;“提前停止”不是“发生错误”。

四、需要逐个拉取时再使用 iter.Pull

普通遍历优先使用 for range。当两个序列需要并行比较,或算法天然需要“取一个、判断一个、再取下一个”时,才把 push 形式转换成 pull 形式。

func Same[T comparable](left, right iter.Seq[T]) bool {
	nextLeft, stopLeft := iter.Pull(left)
	defer stopLeft() // 提前返回时也要停止左侧生产者。
	nextRight, stopRight := iter.Pull(right)
	defer stopRight() // 对右侧序列执行同样的清理。

	for {
		lv, lok := nextLeft()
		rv, rok := nextRight()
		if lok != rok {
			return false // 一个序列先结束,长度不同。
		}
		if !lok {
			return true // 两边同时结束且此前值都相等。
		}
		if lv != rv {
			return false // 当前值不同,立即结束比较。
		}
	}
}

iter.Pull 返回 nextstop。即使当前代码看起来一定会读到结尾,也保留 defer stop() 更稳妥,因为未来的提前返回不会遗漏清理。

五、常见边界与工程选型

场景推荐方式需要确认的边界
顺序消费一组值iter.Seq + for rangeyield=false 后立即停止
键值对或索引值iter.Seq2两个值必须来自同一次迭代
并行比较两个序列iter.Pull每个 stop 都要调用
需要重复读取的数据返回可重复创建的 Seq流式、文件等单次迭代要写清文档

如果序列很短、调用方本来就需要随机访问,普通切片循环通常更直观;iter.Seq 的价值在于统一容器遍历接口、延迟产生数据和让提前停止沿调用链传播。先确定资源生命周期,再决定是否引入迭代器。

相关问题

iter.Seq 是不是已经把数据存起来了?

不是。它保存的是产生数据的函数,调用 for range 时才逐项执行。

yield 返回 false 后还能继续调用吗?

不应该继续调用。官方文档明确把它定义为停止信号,生产者应立即返回并完成清理。

什么时候不用 iter.Pull?

只有顺序消费或可以直接 break 的场景不用 Pull;Pull 主要服务于需要手动控制“下一项”的算法。

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