资讯动态

lo (samber/lo) 中的 Union 助手:基于 Go 泛型的变参集合并集与去重

发布时间:2026/9/13 19:23:32 来源:尧图企业网站定制
lo (samber/lo) 中的 Union 助手基于 Go 泛型的变参集合并集与去重【免费下载链接】lo A Lodash-style Go library based on Go 1.18 Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo本文围绕 lo 库A Lodash-style Go library based on Go 1.18 Generics的Union助手展开先讲清它的函数签名、返回值语义与基本用法再结合 intersect.go 源码剖析其小输入线性扫描 大输入哈希集合的双路径实现并覆盖 intersect_test.go 中的完整测试矩阵与 benchmark/core_intersect_bench_test.go 的基准设计。读完后你可以准确使用Union及其变体UnionBy/UnionByErr并理解其内部在什么规模下会选择哪种去重策略。1. 核心语义保留相对顺序的全集去重Union的定义位于 intersect.go文档 frontmatter 中标注为sourceRef: intersect.go#L235当前源码中该函数位于第 365 行起// Union returns all distinct elements from given collections. // result returns will not change the order of elements relatively. func Union[T comparable, Slice ~[]T](lists ...Slice) Slice一句话概括其契约来自 docs/data/core-union.md 原文Returns all distinct elements from given collections while preserving relative order.即接收任意多个同类型集合变参lists ...Slice合并其中的全部不同distinct元素重复值只保留一次结果保持元素首次出现的相对顺序不排序、不重排元素类型必须满足comparable可用比较这也是文档签名func Union[T comparable, Slice ~[]T](lists ...Slice) Slice的约束来源。文档给出的最小示例lo.Union([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}, []int{0, 10}) // []int{0, 1, 2, 3, 4, 5, 10}注意0、2虽然在三个列表中重复出现结果里各只保留一次且位置由第一次出现决定10追加在末尾。2. 签名参数与类型约束详解参数/约束含义T comparable元素类型必须可比较合法。这保证了Union可以直接用哈希集合作为 seen-set也适用于float64等类型遵循 Go 语义NaN永远不与自身相等因此既不会被判定为重复也不会因 map 键语义而混入结果Slice ~[]T返回类型与输入切片类型一致传入[]int得[]int甚至自定义类型type myStrings []string传入后仍返回myStrings而非[]stringlists ...Slice可变参数可以传 1 个、2 个或任意多个集合集合可以含空集合Union对其不敏感返回值Slice去重后的新切片不修改任何输入切片其中类型保持这一点有专门的测试佐证intersect_test.goTestUnion的type preserved子测试type myStrings []string allStrings : myStrings{, foo, bar} nonempty : Union(allStrings, allStrings) // nonempty 的类型是 myStrings而不是 []string对需要自定义切片类型的调用方而言这意味着Union的结果可以直接赋回原类型变量无需类型断言。3. 源码级实现unionSmall 与 unionLarge 双路径Union并非单一实现而是按总元素量在两条路径间切换intersect.go 第 357–409 行// unionSmallThreshold is the max total element count for which deduping by scanning the // already-built result beats maintaining a seen-set: the result slice is allocated either // way, so below this size the seen-map is pure overhead. const unionSmallThreshold 8 func Union[T comparable, Slice ~[]T](lists ...Slice) Slice { var capLen int for _, list : range lists { capLen len(list) } if capLen unionSmallThreshold { return unionSmall(lists, capLen) } return unionLarge(lists, capLen) }关键点阈值判断依据是所有列表元素总数之和而非单列表长度。unionSmallThreshold 8即所有输入加起来不超过 8 个元素时走小路径。capLen同时用于预分配两条路径都make(Slice, 0, capLen)以总长度为容量上界一次性分配结果切片避免增长过程中的反复扩容。3.1 大输入路径 unionLarge哈希 seen-setfunc unionLarge[T comparable, Slice ~[]T](lists []Slice, capLen int) Slice { result : make(Slice, 0, capLen) seen : make(map[T]struct{}, capLen) for i : range lists { for j : range lists[i] { if _, ok : seen[lists[i][j]]; !ok { seen[lists[i][j]] struct{}{} result append(result, lists[i][j]) } } } return result }用map[T]struct{}记录已见元素struct{}{}零字节值是 Go 中集合惯用法无额外数据开销按列表顺序、元素顺序遍历首次见到的元素立即追加进result这直接对应了保持相对顺序的语义总体时间复杂度 O(N)N 为总元素数。3.2 小输入路径 unionSmall线性扫描免 map 分配func unionSmall[T comparable, Slice ~[]T](lists []Slice, capLen int) Slice { result : make(Slice, 0, capLen) for i : range lists { for j : range lists[i] { if !Contains([]T(result), lists[i][j]) { result append(result, lists[i][j]) } } } return result }小路径不建 map而是复用同文件的 Contains线性扫描对已构建的结果做去重检查。源码注释解释了动机结果切片无论如何都要分配元素总量很小时维护 seen-map 的哈希与分配开销是纯浪费几次扫描反而更便宜。从源码结构看这一小阈值线性扫描 大输入哈希集合的取舍模式在IntersectintersectSmallProduct 64、DifferencedifferenceSmallThreshold 8、WithoutwithoutSmallExcludeThreshold 4中反复出现是 lo 在集合类助手中统一的性能策略。3.3 测试对两条路径的显式覆盖intersect_test.go 中TestUnion的 12 组表驱动用例覆盖了双列表有重叠、双列表无重叠、第二列表为空、内部含重复、两表完全相同、两表皆空、三列表有重叠/无重叠/两空/完全相同、三表皆空等边界例如{name: three lists with overlap, lists: [][]int{{0, 1, 2, 3, 4, 5}, {0, 2, 10}, {0, 1, 11}}, expected: []int{0, 1, 2, 3, 4, 5, 10, 11}}, {name: both lists empty, lists: [][]int{{}, {}}, expected: []int{}},并且专门用两个子测试确保两条路径都保持返回类型type preserved小输入3 个元素走unionSmall路径断言myStrings类型保持type preserved: map path (large input)用 9 个元素使总长超过unionSmallThreshold强制走unionLarge的 map 路径同样断言类型保持。此外 benchmark/core_intersect_bench_test.go 的BenchmarkUnion同时基准了大lengths序列和两个 4 元素列表总量 8恰好落在小扫描阈值内的small子基准与该双路径设计一一对应。4. 变体UnionBy 与 UnionByErrUnion文档的 frontmatter 通过similarHelpers关联了 core-unionby.md 与 core-unionbyerr.md。当元素是否相同不能直接用表达时如按某字段归并、按首字符分组需要使用自定义键选择器。4.1 UnionBy按键去重func UnionBy[T any, V comparable, Slice ~[]T](iteratee func(item T) V, lists ...Slice) Slice元素类型T any不再要求 comparable只要键V可比较即可结果值取自该键首次出现的那个元素源码注释Result values are chosen from the first collection in which the value occurs。文档示例与 intersect_test.goTestUnionBy的首条用例一致lo.UnionBy(func(i int) int { return i / 2 }, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}) // []int{0, 2, 4, 10}0/1同键 02/3同键 14/5同键 210键 5 —— 每键只保留首个元素。字符串示例lo.UnionBy(func(s string) string { return s[:1] }, []string{foo, bar}, []string{baz}) // []string{foo, baz}按首字母归并foo、bar、baz的键分别是f、b、bbar与baz同键保留先出现的bar……但示例结果输出为{foo, baz}的原因见下foo键f先入集bar键b第二次出现前并无其他b键按首次出现规则bar应被保留。这里需要以仓库测试为准——TestUnionBy的 two lists with overlap 用例lists {0,1,2,3,4,5},{0,2,10}i/2键期望[]int{0, 2, 4, 10}与上面 int 示例完全吻合可放心复制运行。实现上UnionBy没有小/大双路径恒用map[V]struct{}seen-setintersect.go 第 414–435 行因为键由迭代器产生、无法预先判断规模收益。4.2 UnionByErr可返回错误的键选择器func UnionByErr[T any, V comparable, Slice ~[]T](iteratee func(item T) (V, error), lists ...Slice) (Slice, error)与UnionBy相同但 iteratee 可以返回 error遇到第一个错误立即停止返回nil结果与该 error源码if err ! nil { return nil, err }。文档示例lo.UnionByErr(func(i int) (int, error) { if i 42 { return 0, errors.New(invalid value) } return i / 2, nil }, []int{0, 1, 2}, []int{42}) // []int{0, 1}, error(invalid value)正常路径lo.UnionByErr(func(i int) (int, error) { return i / 2, nil }, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}) // []int{0, 2, 4, 10}, nilintersect_test.go 的TestUnionByErr对错误路径做了精确验证用一个计数器包装的errFunc在i 2时返回assert.AnError断言result为nil、错误可ErrorIs匹配并且 iteratee恰好停在出错元素wantCallCount分别为 3 与 6验证should stop at first error。这一细节在调用昂贵或可能失败的键提取逻辑如数据库查找、JSON 解析时很重要一旦出错后续元素不会被继续处理。5. 行为语义边界与常见误用结合文档、源码与测试Union的几个容易踩坑的边界不检查顺序也不排序结果顺序 首次出现顺序。Union([]int{2, 1}, []int{0})得到[]int{2, 1, 0}。输入为空集合合法Union对空切片不做特殊处理空列表贡献 0 个元素测试 three lists two empty 验证Union({0..5}, {}, {}) {0..5}。完全无参调用Union()不传任何列表时capLen 0 8走unionSmall返回一个长度为 0 的非 nil 空切片。元素需 comparable结构体可直接参与只要字段全部 comparablemap、slice 类型元素不能直接作为T使用此时应改用UnionBy生成可比较的键。与 Uniq 的区别Union是跨多个集合的并集去重若只需对单个切片内部去重用Uniq/UniqBy见 docs/docs/core/slice.md。文档 frontmatter 也将core#slice#uniq、core#slice#uniqby列为similarHelpers并关联 core-intersect.md、core-difference.md、core-without.md 作为同族集合运算。6. 快速上手与验证方式在支持 Go 1.18 的模块中引入github.com/samber/lo后即可使用本仓库 go.mod 声明的模块路径即为github.com/samber/lo。最小可运行示例package main import ( fmt github.com/samber/lo ) type User struct{ ID int; Name string } func main() { // 基本并集去重保持相对顺序 ids : lo.Union([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}, []int{0, 10}) fmt.Println(ids) // [0 1 2 3 4 5 10] // 按键归并同一部门只保留首个员工 users : lo.UnionBy( func(u User) string { return u.Name[:1] }, []User{{1, Ann}, {2, Bob}}, []User{{3, Cindy}}, ) fmt.Println(users) // [{1 Ann} {3 Cindy}] }验证手段与仓库自身一致# 运行 Union 相关单元测试表驱动 类型保持 两条实现路径 go test -run TestUnion ./... # 查看 Union 基准大输入 small 小扫描路径子基准 go test -bench BenchmarkUnion -benchmem ./benchmark/TestUnion、TestUnionBy、TestUnionByErr分别位于 intersect_test.go 的 466、519、563 行起BenchmarkUnion位于 benchmark/core_intersect_bench_test.go 第 155 行起均可直接运行复核本文所述行为。7. 小结与相关助手助手签名要点适用场景UnionT comparable无迭代器元素可直接比较的变参并集去重UnionByT any 键函数按自定义键归并保留每键首个元素UnionByErr键函数可返回 error键提取可能失败首个错误即中止并返回nil, err三者均实现在 intersect.go文档页面入口为 docs/docs/core/intersect.mdcore 包 intersect 子类的助手列表页。与Union语义互补的集合运算还有Intersect交集、Difference对称差、Without排除指定值实现与测试同样集中在同一文件中便于横向对照阅读。【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价