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

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 仍然可以保持不变。

Go 自定义集合中 Set、内部 map、All 方法、iter.Seq 与 for range 调用方的静态边界关系
图1:自定义集合通过 All 返回 iter.Seq,把内部 map 与调用方的 for range 解耦。

调用方用 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() 收尾。

Go iter.Seq 中 range consumer、break 条件、yield bool 与 All 闭包之间的提前停止和重复遍历关系
图2:调用方的 break 通过 yield 的 false 语义终止当前遍历,All 每次返回新的闭包以支持再次遍历。

空集合、重复遍历和 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 让集合保留遍历实现权,也能自然响应提前停止。

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