lo 的 it.FindUniques:从 Go 1.23+ 迭代器序列中筛出仅出现一次的元素

发布时间:2026/9/13 17:20:35
lo 的 it.FindUniques:从 Go 1.23+ 迭代器序列中筛出仅出现一次的元素 lo 的 it.FindUniques从 Go 1.23 迭代器序列中筛出仅出现一次的元素【免费下载链接】lo A Lodash-style Go library based on Go 1.18 Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo本篇指南围绕lo库it子包中的FindUniques辅助函数展开它接收一个 Go 1.23 风格的迭代器序列返回一个只包含在原集合中恰好出现一次的元素的序列同时保持元素的原始出现顺序。读完本文你将掌握该函数的完整签名、两种典型用法整数序列与字符串序列、自定义唯一性判定的变体FindUniquesBy以及它在源码层面两遍扫描 重复状态 map的实现原理、内存代价和与核心包lo.FindUniques切片版本的差异。函数签名与类型约束FindUniques定义在 it/find.go完整签名为func FindUniquesT comparable, I ~func(func(T) bool) I三个关键点T comparable元素类型必须满足comparable约束可用比较因为去重判定依赖值相等性。整数、字符串、可比较的 struct 等均可直接传入map、slice 等不可比较类型不行。I ~func(func(T) bool)参数不是收窄为iter.Seq[T]而是用类型形参I约束为结构上是func(func(T) bool)的函数类型。这意味着你可以传入任何满足该函数签名的命名序列类型而返回值是同一个类型I。输入序列被透传输出也是同类型序列例如传入自定义类型type myStrings iter.Seq[string]返回的仍然是myStrings类型。这一点由 it/find_test.go 中的preserves iterator type子测试专门验证type myStrings iter.Seq[string] allStrings : myStrings(values(, foo, bar)) nonempty : FindUniques(allStrings) is.IsType(nonempty, allStrings, type preserved)需要注意的是it包的文件头带有//go:build go1.23见 it/find.go 首行即整个it子包依赖 Go 1.23 引入的iter标准库能力使用前提是项目使用 Go 1.23 及以上版本。基本用法从序列中筛出唯一元素FindUniques返回一个序列其中只包含在原集合中仅出现一次的元素——出现多次的重复项被整体剔除而非只保留一份这与Uniq类去重保首的语义不同。以下两个示例完整继承自官方文档 docs/data/it-finduniques.md。整数序列seq : func(yield func(int) bool) { _ yield(1) _ yield(2) _ yield(2) _ yield(3) _ yield(4) _ yield(4) } uniqueSeq : it.FindUniques(seq) var result []int for v : range uniqueSeq { result append(result, v) } // result contains 1, 3 (elements that appear only once)输入为1, 2, 2, 3, 4, 42和4各出现两次被剔除1和3只出现一次按原始顺序保留结果为[1, 3]。字符串序列seq : func(yield func(string) bool) { _ yield(apple) _ yield(banana) _ yield(apple) _ yield(cherry) } uniqueSeq : it.FindUniques(seq) var result []string for v : range uniqueSeq { result append(result, v) } // result contains banana, cherry (unique elements)apple出现两次被剔除结果为[banana, cherry]。在仓库的示例测试 it/find_example_test.go 中还展示了一种基于slices.Values的等价写法和收集结果的方式func ExampleFindUniques() { list : slices.Values([]int{1, 2, 2, 3, 3, 3, 4, 5}) result : FindUniques(list) fmt.Printf(%v, slices.Collect(result)) // Output: [1 4 5] }这里输入1, 2, 2, 3, 3, 3, 4, 5中2出现 2 次、3出现 3 次均被剔除无论重复多少次都不保留输出[1 4 5]。源码实现两遍扫描与重复状态 mapFindUniques本身只有一行实现委托给FindUniquesBy并传入恒等转换函数// FindUniques returns a sequence with all the elements that appear in the collection only once. // The order of result values is determined by the order they occur in the collection. // Will iterate through the entire sequence before yielding and allocate a map large enough to hold all distinct elements. // Long heterogeneous input sequences can cause excessive memory usage. func FindUniquesT comparable, I ~func(func(T) bool) I { return FindUniquesBy(collection, func(item T) T { return item }) }引自 it/find.go真正干活的是 FindUniquesByfunc FindUniquesByT any, U comparable, I ~func(func(T) bool) U) I { return func(yield func(T) bool) { isDupl : make(map[U]bool) for item : range collection { key : transform(item) duplicated, seen : isDupl[key] if !duplicated { isDupl[key] seen } } for item : range collection { key : transform(item) if duplicated : isDupl[key]; !duplicated !yield(item) { return } } } }从源码结构看实现分为两个阶段第一遍统计重复状态。遍历整个输入序列用map[U]bool记录每个键的状态。键第一次出现时seen为 false值写入 false再次出现时把值翻转为 true标记为重复。注意一旦某个键被标记为重复后续出现不再更新——每个键最多经历一次false → true的状态迁移。第二遍产出结果。再次遍历输入序列只yield那些状态仍为 false即只出现过一次的元素。第二个for循环中!yield(item)为真时立即return说明结果序列支持提前中断调用方在for v : range uniqueSeq中break时底层迭代随即停止。由此可以确认源码注释中的两条重要特性先扫完再产出FindUniques是重放型而非流式型辅助函数——返回的序列在被首次迭代之前底层已完整消费过一遍输入来建立重复状态表。因此输入序列必须是可多次迭代的例如slices.Values每次迭代都从头开始可以安全地调用两次range但如果输入是一个只能消费一次的 channel 派生序列第二遍将取不到数据。内存开销与去重元素数成正比map 中保存所有不同元素的状态源码注释明确警告 Long heterogeneous input sequences can cause excessive memory usage。对几乎全不同的超长序列这张状态表会相当大使用时需要评估。FindUniquesBy自定义唯一性判定标准当元素本身不是comparable例如结构体、map或者你希望按某个字段判定相同时使用变体 FindUniquesByfunc FindUniquesByT any, U comparable, I ~func(func(T) bool) U) Itransform对每个元素求值出一个comparable的键U唯一性按该键计算但输出的是原始元素。仓库示例测试 it/find_example_test.go 演示了按年龄筛唯一用户func ExampleFindUniquesBy() { type User struct { Name string Age int } users : slices.Values([]User{ {Name: Alice, Age: 25}, {Name: Bob, Age: 30}, {Name: Charlie, Age: 25}, {Name: David, Age: 30}, {Name: Eve, Age: 35}, }) result : FindUniquesBy(users, func(user User) int { return user.Age }) fmt.Printf(%d, len(slices.Collect(result))) // Output: 1 }5 个用户中 25 岁有 2 人、30 岁有 2 人只有Eve35 岁的年龄是唯一的所以结果长度为 1。注意User本身未实现comparable约束也无妨——FindUniquesBy的T any不限制元素类型约束落在转换出的键U comparable上。单元测试 TestFindUniquesBy 用取模 3 作为转换函数进一步覆盖了各种形态用例输入转换预期输出all unique keys[0, 1, 2]i % 3[0, 1, 2]键 0、1、2 均唯一one unique key[0, 1, 2, 3, 4]i % 3[2]只有键 2 的 2 和 5 中仅 2 出现……即键 2 只出现一次no unique key[0, 1, 2, 3, 4, 5]i % 3nil键 0、1、2 各出现两次empty[]i % 3nil空序列返回空序列该测试还通过assertSeqSupportBreak定义于 it/lo_test.go验证了结果序列支持提前中断迭代。测试覆盖与边界行为核心测试 TestFindUniques 覆盖了四种典型输入形态可作为行为契约参考tests : []struct { name string input []int expected []int }{ {name: all unique, input: []int{1, 2, 3}, expected: []int{1, 2, 3}}, {name: some duplicates, input: []int{1, 2, 2, 3, 1, 2}, expected: []int{3}}, {name: all duplicates, input: []int{1, 2, 2, 1}, expected: nil}, {name: empty, input: []int{}, expected: nil}, }全部唯一原样按序返回部分重复1、2重复被剔除只剩3全部重复 / 空输入返回空结果收集为nil切片。结合实现可知输出顺序严格等于输入顺序第二遍扫描按输入顺序 yield空序列输入不会 panic直接得到一个空序列。与核心包 lo.FindUniques 切片版的差异lo根包提供针对切片输入的对应版本 lo.FindUniques签名是func FindUniques[T comparable, Slice ~[]T](collection Slice) Slice直接返回切片。从源码结构看切片版做了一个it版没有的优化按规模分派两条路径find.go// findSmallThreshold is the max collection size for which a nested O(n²) scan // (avoiding the map used in the large-collection path) is faster than hashing // and allocating for a map-based implementation. const findSmallThreshold 8小规模长度 ≤ 8走findUniquesSmall用嵌套线性扫描对每个元素统计出现次数避免 map 哈希与分配O(n²) 但零 map大规模走findUniquesLarge同样是先建map[T]bool重复状态表、再按序收集非重复元素的两遍结构且额外统计重复数来预分配结果切片的精确容量len(isDupl)-duplicates。两条路径的最终语义与it版本完全一致保留出现一次的元素、顺序不变。选型建议数据源本身是迭代器/生成器iter.Seq或自定义 yield 函数用it.FindUniques/it.FindUniquesBy数据源是已有切片用lo.FindUniques小集合时还能省掉 map 开销若你关心的是找出重复项本身而非唯一项同文件中的 it.FindDuplicates 与 lo.FindDuplicates 是互补函数语义为返回每个重复元素的首次出现。小结it.FindUniques是lo迭代器子包中对iter.Seq风格序列做唯一项筛选的辅助函数T comparable保证值可比较I ~func(func(T) bool)保证输入输出序列类型一致实现对输入做两遍扫描第一遍以map[U]bool建立seen/duplicated状态第二遍按原序 yield 状态为唯一的元素并支持调用方提前中断迭代。其代价是先完整消费一次输入、并为每个不同键分配 map 槽位处理几乎全唯一长序列时需留意内存。需要按字段或按自定义规则判定唯一性时改用FindUniquesBy并提供转换函数即可已有切片数据时可优先选择带小规模 O(n²) 优化路径的lo.FindUniques。相关实现与测试分别位于 it/find.go、it/find_test.go 与 it/find_example_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),仅供参考