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

Go iter.Seq 怎样给自定义集合提供范围迭代器完成一个可复用的最小示例

来源:17golang原创

时间:2026-11-07 03:06:07 478浏览 收藏

如果一个项目有自定义集合,调用方通常希望像遍历切片一样写 for value := range collection.All(),而不是暴露内部切片。Go 1.23 起,函数迭代器可以直接作为 for range 的范围表达式;给集合提供一个返回 iter.Seq[T] 的方法,就能把遍历入口固定成一份可复用 API。

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

要点速览
  • iter.Seq[T] 是接收 yield func(T) bool 的函数类型,集合负责逐项调用它。
  • yield 返回 false 时必须立即停止继续推送,才能正确支持调用方的 break。
  • 集合是否复制输入、元素是否可变、是否允许并发读写,仍然要由自己的 API 约定负责。

先把范围迭代器的职责拆开

当前任务只需要一条单值序列,因此使用 iter.Seq[T],它的形状可以写成 func(yield func(T) bool)。外层函数不返回元素切片,而是拿到调用方提供的 yield 后逐项推送;yield 返回 false,表示调用方不再需要后续值。

这个接口依赖 Go 1.23 的 range-over-function 语法。老版本编译器无法把这类函数当作 range 表达式,生产项目应把模块最低 Go 版本、CI 镜像和开发机工具链统一起来。

Go 自定义集合、iter.Seq、yield 回调与 for range 消费者之间的静态关系说明图
图1:结构说明图,查看集合内部数据、All 方法、iter.Seq 与 yield 回调的边界;这不是运行截图。

给泛型集合实现 All 方法

下面的集合用私有切片保存值,并通过 All 暴露只读式遍历入口。构造函数复制输入切片,是为了避免调用方之后修改原切片时悄悄改变集合内容;如果业务明确需要共享底层数据,也应把这个取舍写进文档。

package main

import "iter"

// Bag 保存一组值,并通过 All 提供统一的范围迭代入口。
type Bag[T any] struct {
	items []T
}

// NewBag 复制输入切片,避免集合和调用方共享可变的底层数组。
func NewBag[T any](items []T) *Bag[T] {
	cloned := append([]T(nil), items...)
	return &Bag[T]{items: cloned}
}

// All 返回一个单值迭代器,调用方可以直接对它使用 for range。
func (b *Bag[T]) All() iter.Seq[T] {
	return func(yield func(T) bool) {
		if b == nil {
			// nil 集合没有元素,保持安全的空序列语义。
			return
		}
		for _, item := range b.items {
			if !yield(item) {
				// 调用方 break 后,停止继续读取和推送。
				return
			}
		}
	}
}

这里的关键不是把 items 改成公开字段,而是让 All 返回标准迭代器类型。任何接受 iter.Seq[T] 的适配器都可以继续消费它,例如 slices.Collect 可以把序列收集为新的切片。

用 for range 消费并验证提前停止

调用方不需要知道 yield 是怎样实现的,只需要对 All() 使用普通的单值 range。一旦业务条件满足就 break,编译器会让迭代器收到停止信号,集合的 if !yield(item) 分支随即结束。

package main

import (
	"fmt"
	"slices"
)

func main() {
	bag := NewBag([]string{"alpha", "beta", "gamma"})

	for value := range bag.All() {
		// 业务只需要前两个值,break 会触发迭代器的提前停止边界。
		fmt.Println(value)
		if value == "beta" {
			break
		}
	}

	// 统一的 Seq 还能交给标准库适配器收集成独立切片。
	collected := slices.Collect(bag.All())
	fmt.Println(collected)
}
alpha
beta
[alpha beta gamma]

上面的输出块描述代码示例的预期形状,不是线上运行截图。真正接入项目时,建议为“完整消费”“遇到条件后 break”“空集合”分别写测试,测试重点是结果数量和提前停止后的副作用,而不是把内部切片暴露出来。

Go iter.Seq 被 for range 和 slices.Collect 消费时与集合数据、yield 停止信号的静态边界说明图
图2:关系说明图,查看同一个 All 序列如何连接 for range、slices.Collect 和提前停止边界;这不是运行截图。

生产代码要守住三个边界

第一是数据所有权。示例在构造时复制切片,因此集合不会被外部切片的后续写入直接改变;但元素本身如果是指针、切片或 map,复制的只是元素引用,不能自动变成深拷贝。

第二是并发访问。iter.Seq 只规定迭代协议,不会替集合加锁。若一个 goroutine 正在修改 items,另一个 goroutine 同时调用 All,仍需要外部同步或在集合内部定义锁策略。

第三是提前停止。不要忽略 yield 的返回值,也不要在回调返回 false 后继续访问外部资源。如果迭代器内部以后加入文件、游标或临时 goroutine,应在这个返回分支中完成对应清理。

发布前检查清单

检查项应确认的结果常见误区
工具链模块和 CI 使用 Go 1.23 或更高版本本地能编译,发布镜像仍是旧版本
接口方法返回 iter.Seq[T],而不是自定义函数签名无法复用标准迭代器适配器
停止每次调用 yield 后检查布尔结果break 后仍继续扫描全部数据
所有权文档说明是否复制输入与元素是否深拷贝误以为浅复制能隔离嵌套引用
并发明确读写锁、快照或外部同步责任把迭代器协议当成并发安全保证

常见问题

为什么不直接返回 []T?

返回切片更直观,但会暴露集合的表示方式,也可能让调用方依赖一次性物化结果。iter.Seq 适合把遍历协议和存储结构分开;如果调用方确实需要独立切片,可以在边界处使用 slices.Collect。

All 方法应该命名成 Values 吗?

两者都能表达语义,但 Go 官方迭代器文档建议容器提供一个表示“全部元素”的 All 方法。若集合有多种序列,再用更具体的名称区分,例如 Errors 或 ActiveItems。

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

不应继续。false 就是消费方的停止协议;继续推送会增加无用工作,还可能在迭代器内部引入已经不该发生的资源访问。

小结:给自定义集合增加范围迭代器,核心是让 All 返回 iter.Seq[T],在每次 yield 后尊重停止信号,再把 Go 版本、数据所有权和并发责任写进项目边界。这样调用方保留普通 for range 体验,集合实现也能继续更换内部存储。

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