Go 怎么让自定义集合支持 for range 遍历
来源:17golang原创
时间:2026-09-06 08:05:25 250浏览 收藏
如果一个集合把内部 map 或切片直接暴露出来,调用方很快就会依赖存储细节。更合适的做法是给集合提供一个返回 iter.Seq[T] 的 All 方法:集合自己决定怎样产出元素,调用方仍然可以写出熟悉的 for v := range set.All()。
在支持 range-over-function 的 Go 版本中,自定义集合只要返回iter.Seq[T],并在闭包中逐个调用yield,就能直接被for range遍历;当yield返回false时,集合应立即停止当前遍历。
iter.Seq[T]的形状是func(yield func(T) bool)。- 集合通常用
All() iter.Seq[T]隐藏内部存储。 - 调用方的
break会让后续yield返回false,闭包必须尊重这个信号。
先确认 iter.Seq 为什么能接上 for range
这不是给 for range 增加一个集合接口,而是利用 Go 1.23 引入的函数类型遍历能力。标准库 iter 把单值序列定义成:
type Seq[V any] func(yield func(V) bool)
也就是说,集合返回的不是已经准备好的切片,而是一个“接收回调并产生值”的函数。range 负责提供回调,集合负责在合适的时候调用它。这个协议同时保留了提前结束的语义,比让调用方读取内部 map 更适合做包级 API。
把集合遍历协议写成 iter.Seq
下面用泛型集合保存唯一值。示例只关心遍历协议,集合的新增和查询方法保持简单:
package main
import (
"fmt"
"iter"
)
// Set 保存不重复的元素,内部 map 不暴露给调用方。
type Set[T comparable] struct {
items map[T]struct{}
}
// NewSet 创建一个可以直接写入的集合。
func NewSet[T comparable]() *Set[T] {
return &Set[T]{items: make(map[T]struct{})}
}
// Add 把元素放入集合,重复值不会产生第二份数据。
func (s *Set[T]) Add(v T) {
s.items[v] = struct{}{}
}
// All 返回集合的单值迭代器,方法名表达“所有元素”。
func (s *Set[T]) All() iter.Seq[T] {
return func(yield func(T) bool) {
for v := range s.items {
// false 表示调用方不再需要后续元素。
if !yield(v) {
return
}
}
}
}
func main() {
set := NewSet[string]()
set.Add("go")
set.Add("range")
// 调用方不需要知道集合内部使用了 map。
for v := range set.All() {
fmt.Println(v)
}
}
All 每次调用都创建一个新的闭包,闭包再读取当前集合内容。调用方只依赖 iter.Seq[T],因此以后把内部 map 换成树或其他结构,遍历 API 仍然可以保持不变。

调用方用 for range 消费,并正确处理中途停止
遍历接口最容易遗漏的是提前停止。调用方发现目标值后可以直接 break,此时迭代器收到的 yield 结果会变成 false。因此 All 里的判断不能省略,否则集合仍可能继续做无意义的工作。
// Find 返回第一个满足条件的元素。
func Find[T comparable](set *Set[T], want T) (T, bool) {
// zero 用于没有命中时的返回值。
var zero T
for v := range set.All() {
if v == want {
return v, true
}
}
return zero, false
}
这里不需要自己调用 iter.Pull。只有当消费方不适合写成 range,需要逐个调用 next 时,才考虑把 push 风格的序列转换成 pull 风格,并用 defer stop() 收尾。

空集合、重复遍历和 API 命名怎么定
空集合不需要特殊分支:内部 range 不进入循环,序列自然结束。一个普通的 All 也应支持重复调用;如果底层是不可回退的文件流或网络流,就要在文档中明确标注“单次使用”,避免调用方误以为每次都能从头开始。
| 设计点 | 建议 | 原因 |
|---|---|---|
| 单值集合 | All() iter.Seq[T] | 与常见 range 写法一致 |
| 键值序列 | Scan() iter.Seq2[K, V] | 保留 key 和 value 的配对关系 |
| 多种顺序 | 用 Backward 等名字区分 | 把顺序写进 API,而不是藏在实现里 |
| 中途停止 | 检查 yield 的 bool | 让 break 真正停止底层遍历 |
相关问题
自定义集合一定要叫 All 吗?
不强制,但 All 是集合返回全部元素时的常用约定。若方法只返回城市、倒序元素或过滤结果,使用能表达序列内容的名字更清楚。
iter.Seq 可以修改集合元素吗?
它本身只提供值。需要在遍历中修改时,可以让迭代器返回位置对象,再由位置对象提供受控的修改方法。
为什么不直接返回切片?
切片会暴露快照、复制或内部存储的选择;iter.Seq 让集合保留遍历实现权,也能自然响应提前停止。
-
234 收藏
-
346 收藏
-
131 收藏
-
185 收藏
-
265 收藏
-
429 收藏
-
128 收藏
-
450 收藏
-
402 收藏
-
344 收藏
-
106 收藏
-
366 收藏
-
321 收藏
-
110 收藏
-
397 收藏
-
473 收藏
-
288 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习