
lo 库 FindUniques 详解Go 1.18 泛型实现仅出现一次元素的查找【免费下载链接】lo A Lodash-style Go library based on Go 1.18 Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo导读本文围绕 lo基于 Go 1.18 泛型的 Lodash 风格 Go 库中的FindUniques函数展开。它用于从集合中筛选出只出现一次的元素同时保持元素在原始集合中的出现顺序。读完本文你将掌握FindUniques/FindUniquesBy的签名与用法、它们与Uniq、FindDuplicates等近似函数在语义上的本质区别以及库内部针对小集合与大集合采用的双路径双重扫描与哈希表优化实现并了解其在it迭代器子包中的泛型序列版本。一、函数定位在 find 系列中的角色FindUniques属于 lo 库的find 子系列其数据文件位于 docs/data/core-finduniques.md签名如下func FindUniques[T comparable, Slice ~[]T](collection Slice) Slice它接收一个元素类型可比较comparable的切片返回一个新切片其中仅包含在集合中出现恰好一次的元素且保持原始出现顺序。该函数在数据文件 frontmatter 中登记的相似函数similarHelpers为core#find#finduniquesby带键提取的变体core#find#findduplicates查找重复元素core#find#findduplicatesby按键查找重复元素在 docs/docs/core/find.md 中可以看到整个 find 族函数的完整文档组织。与其他去重/查找函数的区别函数返回内容顺序FindUniques只出现一次的元素保留原始顺序FindDuplicates每个重复元素的第一处出现保留原始顺序Uniq去重后的全部元素保留每个值首次出现保留原始顺序对照示例均来自 docs/data 目录下的数据文件lo.FindUniques([]int{1, 2, 2, 1, 2, 3}) // []int{3} // 只有 3 出现一次 lo.FindDuplicates([]int{1, 2, 2, 1, 2, 3}) // []int{1, 2} // 重复元素的首次出现 lo.Uniq([]int{1, 2, 2, 1}) // []int{1, 2} // 去重后保序二、核心语义与基础用法FindUniques的核心语义一句话即可概括从集合中挑出出现恰好一次的元素并保持它们在原集合中的顺序。package main import ( fmt github.com/samber/lo ) func main() { // 字符串示例重复出现多次的元素被剔除 fmt.Println(lo.FindUniques([]string{apple, banana, apple, cherry})) // 输出: [banana cherry] // 空集合返回空nil切片 fmt.Println(lo.FindUniques([]int{})) // 输出: [] // 全重复集合没有任何唯一元素返回 nil fmt.Println(lo.FindUniques([]int{1, 2, 2, 1})) // 输出: [] }注意几点边界行为均由 find_test.go 中的测试用例覆盖见TestFindUniques_smallScan空集合返回nil全重复集合返回nil全部唯一的集合返回与原集合等价的切片元素及顺序均不变返回值的切片类型被保留如果传入的是命名切片类型如type myStrings []string返回值仍是该命名类型测试用例 type preserved 通过is.IsType验证了这一点这得益于签名中的Slice ~[]T约束。泛型签名解读func FindUniques[T comparable, Slice ~[]T](collection Slice) SliceT comparable元素类型必须是可比较的支持因为内部需要用元素作为 map 的键或进行相等比较Slice ~[]T~波浪号表示底层类型为[]T的任何命名类型都可用这正是返回类型被保留的机制来源。三、按计算键去重FindUniquesBy当需要根据元素的某个属性或计算值判断唯一性时使用FindUniquesBy数据文件见 docs/data/core-finduniquesby.mdfunc FindUniquesBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Sliceiteratee对每个元素调用一次生成一个可比较的键U元素是否唯一由该键的出现次数决定。注意此时元素类型T不再要求comparable只需键类型U可比较即可。// 按模 3的结果分组3→0, 4→1, 5→2, 6→0, 7→1 // 键 0 出现两次3、6键 1 出现两次4、7键 2 出现一次5 lo.FindUniquesBy([]int{3, 4, 5, 6, 7}, func(i int) int { return i % 3 }) // []int{5}再看一个实际场景——按结构体字段去重type User struct { ID int Name string } users : []User{ {ID: 1, Name: Alice}, {ID: 2, Name: Bob}, {ID: 1, Name: Carol}, // 与第一个 User 的 ID 相同 } // 只保留 ID 恰好出现一次的用户 uniqueByID : lo.FindUniquesBy(users, func(u User) int { return u.ID }) // 结果: []User{{2, Bob}}因为 ID1 出现了两次与 FindDuplicatesBy 的对应关系FindDuplicatesBy见 docs/data/core-findduplicatesby.md返回的是每个重复键对应元素的第一次出现与FindUniquesBy互为补集关系// 同样按模 3 分组 lo.FindDuplicatesBy([]int{3, 4, 5, 6, 7}, func(i int) int { return i % 3 }) // []int{3, 4} // 键 0 重复 → 返回 3键 1 重复 → 返回 4四、源码级原理小集合双扫描与大数据哈希表FindUniques的公开入口位于 find.go#L178其内部通过一个阈值常量做分派// 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 func FindUniques[T comparable, Slice ~[]T](collection Slice) Slice { if len(collection) findSmallThreshold { return findUniquesSmall(collection) } return findUniquesLarge(collection) }阈值findSmallThreshold 8find.go#L173当集合长度 ≤ 8 时走小集合路径否则走大集合路径。其设计理由是对小集合而言嵌套 O(n²) 扫描的代价低于为 map 分配内存 哈希计算的固定开销因此小集合路径刻意避免 map 分配。小集合路径findUniquesSmallO(n²) 双扫描func findUniquesSmall[T comparable, Slice ~[]T](collection Slice) Slice { result : make(Slice, 0, len(collection)) for i : range collection { count : 0 for j : range collection { if collection[j] collection[i] { count } } if count 1 { result append(result, collection[i]) } } return result }逻辑对每个位置i内层循环统计与其相等的元素总数计数恰好为 1 时追加到结果。由于元素恰好出现一次的位置即其唯一出现位置自然迭代顺序与原始顺序完全一致无需额外排序。大集合路径findUniquesLargeO(n) 哈希表func findUniquesLarge[T comparable, Slice ~[]T](collection Slice) Slice { isDupl : make(map[T]bool, len(collection)) duplicates : 0 for i : range collection { duplicated, seen : isDupl[collection[i]] if !duplicated { isDupl[collection[i]] seen if seen { duplicates } } } result : make(Slice, 0, len(isDupl)-duplicates) for i : range collection { if duplicated : isDupl[collection[i]]; !duplicated { result append(result, collection[i]) } } return result }该实现以 map 的 value 作为状态机首次遇到某元素时写入false已见但尚未重复第二次遇到时读出seen true将 value 改为true已确认重复同时duplicates此后再遇到时duplicated已为true直接跳过。第一个循环结束后len(isDupl)是不同元素的种类数duplicates是出现至少两次的元素种类数len(isDupl) - duplicates即为恰好出现一次的元素个数用作结果切片预分配容量make(Slice, 0, ...)避免追加过程中的多次扩容。第二个循环再次遍历原集合凡 map 中标记为false未重复的元素按原始顺序追加进结果从而一次哈希遍历 一次过滤遍历整体 O(n)。FindUniquesBy同样采用findUniquesBySmall/findUniquesByLarge双路径分派find.go#L238差异仅在于用iteratee(item)生成的键U代替元素本身作为 map 的键。其 small 路径会先一次性预计算所有键保证每个元素上 iteratee 恰好被调用一次与 map 路径的调用次数契约一致再做嵌套计数扫描find.go#L280-L301。复杂度小结路径触发条件时间复杂度额外空间findUniquesSmalllen ≤ 8O(n²)O(n)结果切片无 mapfindUniquesLargelen 8O(n)O(distinct)map 结果切片五、测试验证双路径均被覆盖find_test.go 中通过两组测试分别覆盖两条路径TestFindUniques_smallScanfind_test.go#L503所有用例集合长度均 ≤findSmallThreshold覆盖 all unique全部唯一、one unique among duplicates[]int{1, 2, 2, 3, 1, 2}→[3]、no unique[]int{1, 2, 2, 1}→nil、empty collectionnil四种边界并用type myStrings []string断言返回类型被保留。TestFindUniques_largefind_test.go#L546故意使用 12 个元素的集合超过阈值 8强制进入 map 路径并先用is.Greater(len(tt.collection), findSmallThreshold, ...)做前提校验例如[]int{10, 20, 30, 20, 40, 50, 60, 70, 80, 90, 40, 10}→[]int{30, 50, 60, 70, 80, 90}。FindUniquesBy同样有TestFindUniquesBy_smallScan与TestFindUniquesBy_large两组测试。这些测试可在仓库根目录直接运行go test -run TestFindUniques -v .六、it 子包泛型迭代器版本 FindUniques除切片版本外lo 的it子包还提供了基于 Go 1.23 迭代器func(func(T) bool)风格的 sequence的泛型版本见 it/find.go#L167 与数据文档 docs/data/it-finduniques.mdfunc FindUniquesT comparable, I ~func(func(T) bool) I它内部直接委托给it.FindUniquesByit/find.go#L177实现同样是先遍历整个序列填充 map 状态再第二次遍历过滤。文档注释特别提示两个使用注意点该函数在产出第一个结果前会先完整迭代一遍输入序列会分配一个足以容纳全部不同元素的 map过长的异构输入序列可能导致内存占用过高。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 包含 1, 3仅出现一次的元素七、常见误用与实战建议不要与Uniq混淆Uniq保留每个值首次出现结果为{1, 2}FindUniques只保留出现一次的值结果为{3}。若只想去重应使用Uniq若想找出真正唯一的元素如筛选日志中的独有错误码、找出只出现一次的 IP才使用FindUniques。小集合仍可放心使用findUniquesSmall的 O(n²) 双扫描只影响 ≤ 8 个元素的小切片代价可忽略大集合自动切换到 O(n) 哈希路径因此直接调用公开 API 无需关心阈值细节。FindUniquesBy的迭代器调用次数小路径下iteratee对每个元素恰好调用一次键被预计算若你的 iteratee 有副作用或开销较大这一点可以放心。内存提示it.FindUniques需要先完整消费整个序列并持有全量 distinct 键的 map不适合超长或无限序列切片版本则一次性持有输入与输出两份切片。延伸阅读同系列函数FindDuplicatesfind.go#L306、FindDuplicatesByfind.go、Uniqslice.go完整文档目录docs/docs/core/find.md、docs/docs/iter/find.md迭代器版本测试it/find_test.go基准测试benchmark/core_find_bench_test.go、benchmark/it_find_bench_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),仅供参考