lo 库 it.Values:把多个 map 的值拼接成 Go 1.23 iter.Seq 值序列
【免费下载链接】lo💥 A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo
导读
it.Values是 lo(Lodash-style Go 库)的迭代器(it)子包中一个面向 map 值抽取的高阶泛型函数:它接受一个或多个map[K]V,并把所有 map 的 value 拼接成一个iter.Seq[V]惰性序列。本文以 docs/data/it-values.md 为核心,结合 it/map.go 的源码实现与 it/map_test.go 的测试用例,完整讲解it.Values的函数签名、多 map 拼接语义、惰性求值原理、与it.Keys/it.UniqValues的配套关系,以及在实际业务中的典型用法,读完即可在 Go 1.23+ 项目中直接落地使用。
函数签名与核心语义
it.Values的完整签名如下(见 docs/data/it-values.md 与 it/map.go):
func ValuesK comparable, V any iter.Seq[V]要点拆解:
- 类型参数:
K必须是comparable(map 键的硬性约束),V可以是任意类型(any)。这意味着值可以是结构体、切片、指针、接口等任何 Go 类型。 - 可变参数:
in ...map[K]V允许传入 0 个、1 个或多个 map。传入 0 个 map 时返回空序列;传入多个 map 时按参数顺序依次遍历并"拼接"它们的值。 - 返回值:
iter.Seq[V]是 Go 1.23 引入的标准库迭代器类型,与for range直接配合使用。
顺序语义:值的顺序不是确定性的
需要特别强调的是:it.Values返回的值序列顺序并不保证稳定。原因在于 Go 语言本身规定 map 的迭代顺序是未定义的——源码实现内部直接使用for _, v := range in[i](it/map.go),因此值的产出顺序完全取决于底层 map 的迭代顺序。如果你的下游逻辑依赖顺序,请先对结果排序(见下文"排序后使用"一节)。
源码级实现原理
it.Values的实现非常精简,整个函数体只有一个惰性闭包(见 it/map.go):
func ValuesK comparable, V any iter.Seq[V] { return func(yield func(V) bool) { for i := range in { for _, v := range in[i] { if !yield(v) { return } } } } }从中可以提炼出三个关键设计:
惰性求值(lazy evaluation):调用
it.Values(m1, m2)本身不会立即遍历任何 map,它只是返回一个闭包。真正的遍历发生在消费者开始for v := range seq时。map 规模再大,构造序列的开销也近乎为零,适合流水线式链式处理。及早终止(early termination):闭包接收的
yield函数返回bool。当消费者使用break或return提前退出循环时,yield返回false,实现随即return停止迭代,避免无谓的遍历。测试 it/lo_test.go 中的assertSeqSupportBreak专门验证了这一点:对it.Values产生的序列执行break和return都不会 panic,且序列可被重复遍历(iter.Seq的每次for range都会重新从第一个 map 开始)。零中间分配:实现不创建新的切片或缓冲,只是按参数顺序对外层
in切片、对内层每个 map 做双重循环。相比先收集所有值再拼接的传统写法,内存占用更优。
完整示例:从文档到可运行代码
文档 docs/data/it-values.md 给出的示例演示了单/多 map 的核心用法,完整还原如下:
m1 := map[string]int{ "apple": 1, "banana": 2, } m2 := map[string]int{ "cherry": 3, "date": 4, } valuesSeq := it.Values(m1, m2) var result []int for v := range valuesSeq { result = append(result, v) } // result contains values from both maps对应地,it/map_example_test.go 提供了带确定性断言的官方示例:
func ExampleValues() { kv := map[string]int{"foo": 1, "bar": 2} kv2 := map[string]int{"baz": 3} result := slices.Collect(Values(kv, kv2)) sort.Ints(result) fmt.Printf("%v", result) // Output: [1 2 3] }注意官方示例先调用slices.Collect把序列收拢为切片,再用sort.Ints排序,最后做确定性断言——这正是"map 顺序不确定"语义下的标准实践。
更简洁的惯用写法:slices.Collect
由于iter.Seq与标准库slices包无缝衔接,收集结果可以写得更紧凑:
import "slices" values := slices.Collect(it.Values(m1, m2)) // []int{...},顺序不保证单元测试揭示的边界行为
it/map_test.go 的TestValues用表格驱动覆盖了五类典型场景,是理解函数边界行为的最佳教材:
| 测试场景 | 输入 | 期望结果 | 说明 |
|---|---|---|---|
| 单个 map | {"foo":1, "bar":2} | [1, 2] | 基础用法 |
| 空 map | {} | [] | 空 map 不产生任何值 |
| 多个 map | {"foo":1,"bar":2}, {"baz":3} | [1, 2, 3] | 多 map 值拼接 |
| 无 map(nil) | nil | [] | 不传参数返回空序列,不 panic |
| 跨 map 重复键 | {"foo":1,"bar":2}, {"foo":1,"bar":3} | [1, 1, 2, 3] | 不去重,键重复不影响值输出 |
最后一行非常关键:it.Values不会去重。即使两个 map 拥有相同的键(如两处都有"foo"),两个值1都会被输出。若需要去重语义,请使用配套的it.UniqValues。
与兄弟函数的配套使用
在it子包的 map 工具族中,it.Values与以下函数形成互补(全部定义于 it/map.go):
- it.Keys(it/map.go):同样接受多个 map,产出所有键的拼接序列
iter.Seq[K],结构与it.Values完全对称。 it.UniqValues(it/map.go):对值做去重后再产出序列。实现上先统计所有 map 的总长度total,预分配make(map[V]struct{}, total)作为去重集合,保留"首次出现"的值(见 it/map_test.go 中preserves first-seen order用例),因此要求V comparable。文档 docs/data/it-uniquevalues.md 中有对应说明。it.FilterValues(it/map.go):结合谓词筛选后再产出值序列,是Filter与Values的混合体,适合"只关心满足条件的值"的场景。it.Entries/it.ToPairs(it/map.go):产出iter.Seq2[K, V]键值对序列;it.Values可以看作对Entries结果丢弃键、只留值的特化。it.Seq2ValueToSeq(it/map.go):把任意iter.Seq2[K, V]转换为iter.Seq[V],与it.Values用途相近但输入是序列而非 map。
从文档目录(docs/data/)可以看出,it-keys.md、it-values.md、it-uniqvalues.md、it-filtervalues.md等属于同一iter → map分组,position字段标示了它们在文档侧边栏中的排布顺序。
实战示例
场景一:合并多个配置来源的值列表
// 合并多张配置表,收集所有版本号 cfgA := map[string]string{"redis": "7.2", "mysql": "8.0"} cfgB := map[string]string{"kafka": "3.6", "redis": "7.2"} versions := slices.Collect(it.Values(cfgA, cfgB)) // 可能得到类似 []string{"7.2", "8.0", "3.6", "7.2"} 的顺序不确定集合场景二:流水线链式处理
iter.Seq的惰性特性让it.Values可以嵌入更大的处理链中,配合slices/iter标准库函数继续变换:
import ( "iter" "slices" ) scores := map[string]int{"alice": 88, "bob": 92, "carol": 79} // 惰性链:先取全部值,再筛选及格线以上,最后求和 total := 0 for v := range iter.Filter( it.Values(scores), func(v int) bool { return v >= 60 }, ) { total += v }场景三:配合去重统计
当同一个值出现在多个 map 中时,用it.UniqValues得到唯一值集合:
m1 := map[string]int{"foo": 1, "bar": 2} m2 := map[string]int{"baz": 2} uniq := slices.Collect(it.UniqValues(m1, m2)) // [1, 2](去重后)使用前提与注意事项
- Go 版本要求:
it子包基于iter标准库,源码带有//go:build go1.23构建标签(见 it/map.go),必须在 Go 1.23 及以上版本使用。主库lo本身声明go 1.18(见 go.mod),因此it包是作为独立子包按需引入的。 - 导入路径:
it子包的模块路径为github.com/samber/lo/it,使用时需要单独导入,例如import "github.com/samber/lo/it"。 - 顺序不确定性:再次强调,返回值顺序依赖 Go map 的随机迭代顺序,任何依赖顺序的消费方都必须自行排序(
sort包或slices.Sort)。 - 不去重:需要唯一值时显式改用
it.UniqValues,不要依赖it.Values做去重。 - 序列可重复遍历:每个
iter.Seq每次for range都会从头重新执行闭包遍历逻辑,it.Values产出的序列同样如此,可放心多次消费。
小结
it.Values用十余行泛型代码解决了"从多张 map 中批量提取值"这一高频需求:它把多 map 值拼接、惰性求值、及早终止三个特性浓缩为一个返回iter.Seq[V]的函数,与 Go 1.23 的 range-over-func 生态无缝衔接。配合it.Keys、it.UniqValues、it.FilterValues以及标准库slices.Collect,足以覆盖绝大多数 map 值处理场景。其实现细节与边界行为(不去重、顺序不定、零分配、可 break)均可在 it/map.go 与 it/map_test.go 中找到源码级佐证,值得在阅读源码时反复对照。
【免费下载链接】lo💥 A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考