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

用 iter.Seq2 统一处理键值对过滤与转换

来源:17golang原创

时间:2026-10-09 12:13:55 467浏览 收藏

要统一处理键值对,最实用的做法是把过滤和转换都写成接收、返回 iter.Seq2 的泛型适配器:Filter2 只决定哪些键值对继续传递,Map2 负责把 (K, V) 改成 (K2, V2)。这样,maps.All、slices.All 或自定义容器产出的键值对,都能接入同一套惰性处理链。

这套写法从 Go 1.23 开始可直接用 for ... range 遍历函数迭代器。真正需要守住的边界只有几个:下游 yield 返回 false 后必须立即停止;从 map 取得的顺序不能依赖;收集回 map 时转换后的键必须可比较;转换键时还要考虑不同输入映射到同一键的冲突。

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

Seq2 的最小契约

iter.Seq2[K, V] 的定义可以理解为:迭代器接收一个 yield(K, V) bool 回调,把每一对值交给它;当 yield 返回 false 时,迭代器停止继续生产。它通常承载键和值,也可以承载索引和值。

// Seq2 的等价形状:真实定义位于标准库 iter 包
type Seq2[K, V any] func(yield func(K, V) bool)

项目的 go.mod 语言版本至少要设为 Go 1.23,才能使用 range over function。适配器本身不要负责存储结果,它只包装上游序列并返回新的序列,这样过滤和转换仍然是惰性的。

把过滤与转换拆成两个适配器

Filter2 保持键和值的类型不变;只有谓词返回 true 的键值对才会交给下游。关键不是 if,而是必须把下游的停止信号继续向上传播:

package sequtil

import "iter"

// Filter2 保留 keep 返回 true 的键值对,不提前收集结果。
func Filter2[K, V any](
    seq iter.Seq2[K, V],
    keep func(K, V) bool,
) iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        for k, v := range seq {
            if !keep(k, v) {
                continue
            }
            // 下游不再需要数据时,立即结束上游遍历。
            if !yield(k, v) {
                return
            }
        }
    }
}

Map2 允许键和值同时改变类型。输入键不要求可比较,因为 Seq2 本身只是传递值;只有最终调用 maps.Collect 时,输出键 K2 才必须满足 comparable。

package sequtil

import "iter"

// Map2 把每个 (K1, V1) 转换为新的 (K2, V2)。
func Map2[K1, V1, K2, V2 any](
    seq iter.Seq2[K1, V1],
    convert func(K1, V1) (K2, V2),
) iter.Seq2[K2, V2] {
    return func(yield func(K2, V2) bool) {
        for k, v := range seq {
            nextK, nextV := convert(k, v)
            // 必须尊重下游的提前停止请求,避免多做转换。
            if !yield(nextK, nextV) {
                return
            }
        }
    }
}
iter Seq2、Filter2、Map2、谓词和映射函数的静态调用结构
图1:Seq2 过滤与转换适配器的静态调用结构说明图。Filter2 保持类型,Map2 可以改变键和值的类型,二者都通过 yield 连接上下游。

组合 maps.All、Filter2、Map2 与 maps.Collect

下面用一份服务配置演示完整组合:先保留已启用且限额为正的项目,再统一服务名并把结构体转换成整数限额,最后收集为新 map。

package main

import (
    "fmt"
    "maps"
    "strings"

    "example.com/project/sequtil"
)

type Config struct {
    Enabled bool
    Limit   int
}

func main() {
    source := map[string]Config{
        "Search":  {Enabled: true, Limit: 20},
        "Billing": {Enabled: false, Limit: 10},
        "Export":  {Enabled: true, Limit: 0},
    }

    active := sequtil.Filter2(
        maps.All(source),
        func(_ string, cfg Config) bool {
            // 只保留已经启用且限额有效的配置。
            return cfg.Enabled && cfg.Limit > 0
        },
    )

    normalized := sequtil.Map2(
        active,
        func(name string, cfg Config) (string, int) {
            // 键统一为小写,值只保留消费方需要的限额。
            return strings.ToLower(name), cfg.Limit
        },
    )

    // maps.Collect 在消费边界一次性创建目标 map。
    result := maps.Collect(normalized)
    fmt.Println(result)
}

这条链没有中间切片,也没有让 Filter2 或 Map2 知道输入来自 map。将来换成自定义索引、树节点或数据库游标,只要能够提供符合契约的 iter.Seq2,适配器无需修改。

提前停止必须贯穿整条处理链

调用方可能只想找第一项。只要每层适配器都在 yield 返回 false 时立刻返回,停止信号就能穿过 Map2 和 Filter2 回到数据源:

func First2[K, V any](seq iter.Seq2[K, V]) (K, V, bool) {
    var foundK K
    var foundV V
    found := false

    seq(func(k K, v V) bool {
        foundK, foundV, found = k, v, true
        // 返回 false,通知所有上游适配器停止继续生产。
        return false
    })

    return foundK, foundV, found
}

若适配器忽略 yield 的返回值,结果也许仍然正确,但上游会继续扫描、继续转换,文件、网络或数据库型迭代器还可能继续占用资源。把停止传播当作接口契约,而不是微小的性能优化。

上线前要守住的四个边界

map 的遍历顺序未指定

maps.All 返回键值迭代器,但官方文档明确说明顺序未指定,而且不同调用之间不保证一致。如果输出顺序影响日志、签名、测试快照或用户界面,就不要直接依赖这条序列的顺序;先取得键并排序,再按排序后的键构造稳定序列。

func SortedMapSeq[K cmp.Ordered, V any](m map[K]V) iter.Seq2[K, V] {
    keys := slices.Sorted(maps.Keys(m))
    return func(yield func(K, V) bool) {
        for _, k := range keys {
            // 排序后的键决定稳定顺序,仍要传播停止信号。
            if !yield(k, m[k]) {
                return
            }
        }
    }
}

收集回 map 的键必须 comparable

Map2 可以输出任意键类型,但 maps.Collect 的键参数受 comparable 约束。若转换结果是切片、map 或函数等不可比较类型,应收集成切片结构,或改用稳定的字符串、整数和结构体标识。

转换键可能制造冲突

大小写归一化、截断或复合键降维都可能让多个输入落到同一个输出键。若冲突不可接受,就在收集前显式检测,而不要让“最后保留了哪个值”依赖无序 map 的遍历结果。

func CollectUnique[K comparable, V any](seq iter.Seq2[K, V]) (map[K]V, error) {
    result := make(map[K]V)
    var duplicate error

    seq(func(k K, v V) bool {
        if _, exists := result[k]; exists {
            // 一旦发现重复键就停止,避免结果处于不可解释状态。
            duplicate = fmt.Errorf("duplicate transformed key: %v", k)
            return false
        }
        result[k] = v
        return true
    })

    return result, duplicate
}

单次迭代器不能默认重放

多数序列可以重复遍历,但官方 iter 文档也允许单次迭代器,例如不可倒带的数据流。通用适配器不应偷偷遍历两次,也不应先计数再处理。若业务必须重放,就在明确的消费边界收集数据,并记录这次内存换可重放性的取舍。

maps All、iter Seq2、提前停止、maps Collect、可比较键和顺序约束的静态关系
图2:Seq2 处理链的生产边界静态说明图。提前停止、可比较键与无序 map 分属不同约束,不能由同一个适配器隐式解决。

发布检查清单

检查项正确做法常见风险
语言版本go.mod 至少声明 Go 1.23旧语言版本无法 range 函数迭代器
停止传播每次调用 yield 后检查 bool下游停止后仍继续扫描和转换
遍历次数适配器单次消费上游单次迭代器被意外读空
输出键收集前确认 comparable泛型约束不匹配或无法建 map
键冲突业务敏感时显式检测归一化后静默覆盖
稳定顺序需要时先排序键日志和测试结果漂移

相关问题

Filter2 和 maps.DeleteFunc 有什么区别?

maps.DeleteFunc 直接修改现有 map;Filter2 返回惰性序列,不修改来源,更适合继续组合转换或输出到其他容器。

只处理值时还需要 Seq2 吗?

如果后续完全不需要键,可以先用 maps.Values 得到 iter.Seq[V],再使用单值版过滤和转换。仍要保留键、索引或来源标识时再用 Seq2。

什么时候应该直接写 for range?

只有一处使用、逻辑很短且不需要组合时,直接循环通常更清楚。Seq2 适合多个来源共享同一组过滤、转换和消费规则的场景,不必为了链式外观替换所有普通循环。

需要手动调用 iter.Pull2 吗?

普通过滤与转换不需要。只有消费方必须一次主动拉取一对值,或要并行协调多个序列时,才考虑 Pull2;未消费到序列结尾时还要调用返回的 stop,通常用 defer stop() 保证释放。

把 Filter2 与 Map2 做成小而明确的泛型适配器后,来源、处理和收集就能分开演进。工程质量的关键不在链条写得多长,而在每层都尊重 yield、不假设 map 顺序,并把键冲突和内存收集放到清晰的边界中。

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