lo 库 FindDuplicatesByErr 详解:基于 iteratee 键值查找重复元素并支持错误中止
【免费下载链接】lo💥 A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo
导读
FindDuplicatesByErr是 lo(Lodash-style 的 Go 泛型库)find 子模块中一个"可出错版"的去重查找助手:它借助用户提供的iteratee函数把任意类型元素映射为可比较的键(key),返回每个重复键在集合中首次出现的那一个元素,并保持原始顺序;同时iteratee可以返回error,一旦出错迭代立即中止、函数直接返回该错误。本文以 core-findduplicatesbyerr.md 文档为核心,结合 find.go 的源码实现与 find_test.go 的测试用例,从签名、用法、错误语义、内部两趟扫描原理、与同类助手的对比到类型保持特性,给出完整可落地的实战指南。
函数签名与类型约束
func FindDuplicatesByErr[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) (U, error)) (Slice, error)参数与返回值说明:
| 成员 | 含义 |
|---|---|
collection Slice | 输入集合,Slice ~[]T表示既接受标准切片[]T,也接受任何以[]T为底层类型的具名切片类型(见下方"类型保持"一节) |
iteratee func(item T) (U, error) | 对每个元素调用的键生成函数:把T映射为用于判定重复的键U,同时可附带错误 |
U comparable | 键类型必须可比较,因为内部需要用map[U]bool记录已见状态 |
| 返回值 | (Slice, error):正常时返回重复元素切片与nil;出错时返回nil切片与错误 |
从源码结构看,该函数与 FindDuplicatesBy(文档)是"错误处理变体"关系:FindDuplicatesBy的iteratee只返回键,而FindDuplicatesByErr让iteratee额外返回error,从而在无法继续计算键(如外部 API 调用失败、数据非法)时拥有提前终止的能力。
基本用法:按键查找重复元素
文档给出的典型示例(对应源码第 445-489 行实现)如下:
result, err := lo.FindDuplicatesByErr([]int{3, 4, 5, 6, 7}, func(i int) (int, error) { return i % 3, nil }) // []int{3, 4}, <nil>执行过程逐步拆解:
- 依次计算键:
3 % 3 = 0、4 % 3 = 1、5 % 3 = 2、6 % 3 = 0、7 % 3 = 1; - 键
0与1各出现两次,属于重复键;键2只出现一次; - 结果取每个重复键在集合中首次出现的元素:键
0首次对应3,键1首次对应4,且结果保持原始顺序,因此得到[]int{3, 4}。
要点:返回的不是全部重复元素(那会得到{3, 4, 6, 7}),而是每个重复键的代表元素,其顺序由元素在集合中首次出现的位置决定。
错误处理语义:迭代立即中止
iteratee返回错误时,函数的行为在文档中有明确示例:
result, err := lo.FindDuplicatesByErr([]int{3, 4, 5, 6, 7}, func(i int) (int, error) { if i == 5 { return 0, fmt.Errorf("number 5 is not allowed") } return i % 3, nil }) // []int(nil), error("number 5 is not allowed")关键语义有三点,均与源码实现一一对应:
- 立即中止:源码在第一趟统计(find.go)与第二趟收集(find.go)中都检查了
err != nil,任一阶段出错都直接return; - 返回 nil 切片:出错分支执行
var result Slice; return result, err,即返回零值nil切片,绝不返回"部分结果"造成误判; - 两趟都会触发:由于实现分两趟扫描集合,第二趟中
iteratee出错同样会中止并返回错误。
测试 find_test.go 专门用回调计数器验证了"提前中止"契约:
- 错误发生在第一趟第 0 个元素:
iteratee只被调用 1 次; - 错误发生在第一趟第 2 个元素:调用 3 次后中止;
- 错误发生在第二趟首个元素(输入
{3, 4, 5, 6}、errorAt=3):第一趟完整执行 4 次后,第二趟在第一个元素处出错,总调用次数为 4,注释明确说明"error at first item of second pass"。
测试同时断言出错时返回值为nil(is.Nil(result, "nil should be returned on error"))。
源码原理:两趟扫描 + map 状态机
FindDuplicatesByErr的实现(find.go)采用"统计 + 收集"两趟扫描,与FindDuplicatesBy的findDuplicatesByLarge路径共享同一套 map 状态机思路:
第一趟:统计键的出现次数状态
isDupl := make(map[U]bool, len(collection)) duplicates := 0 for i := range collection { key, err := iteratee(collection[i]) if err != nil { var result Slice return result, err } duplicated, seen := isDupl[key] if !duplicated { isDupl[key] = seen if seen { duplicates++ } } }这里map[U]bool的布尔值记录"该键是否为重复键":键第一次遇到时记录false(未重复),第二次遇到时变为true并累加duplicates计数,之后保持true不再变化。duplicates用于预分配结果切片容量make(Slice, 0, duplicates),避免收集阶段反复扩容。
第二趟:按顺序收集首次出现的重复元素
result := make(Slice, 0, duplicates) for i := range collection { key, err := iteratee(collection[i]) if err != nil { var result Slice return result, err } if duplicated := isDupl[key]; duplicated { result = append(result, collection[i]) isDupl[key] = false } } return result, nil再次扫描集合,当某元素的键被标记为重复时将其收入结果,并把该键标记重置为false,从而确保同一重复键只输出首次出现的元素。
需要特别指出的实现事实:iteratee在每一趟都会对每个元素重新调用一次(两趟共调用约 2×N 次,出错时提前中止),这与findDuplicatesBySmall中"先预计算 keys 再复用"的实现不同(后者将调用次数严格限制为每元素一次,见 find.go)。因此如果iteratee有副作用或开销较大,应留意FindDuplicatesByErr的调用次数特征。
无错误时与 FindDuplicatesBy 行为等价
把iteratee固定返回(key, nil)时,FindDuplicatesByErr与 FindDuplicatesBy 的输出完全一致。二者的定位差异可总结为:
| 对比项 | FindDuplicatesBy | FindDuplicatesByErr |
|---|---|---|
| iteratee 签名 | func(item T) U | func(item T) (U, error) |
| 错误处理 | 无,键计算失败无从感知 | 出错立即中止并返回(nil, err) |
| 典型场景 | 键计算是纯函数、不会失败 | 键计算可能出错(解析、查库、网络请求等) |
测试 find_test.go 覆盖了正常路径的四类输入:发现重复({3,4,5,6,7}→{3,4})、无重复({0,1,2,3,4}→{0,1})、空集合({}→{})、全部重复({0,3,6,9}→{0})。其中"无重复"场景值得注意:当所有键都唯一时,第一趟结束后duplicates == 0,第二趟不会收集任何元素,结果为空切片而非 nil。
实战:类型保持(Named Slice Type)
由于签名使用Slice ~[]T约束,FindDuplicatesByErr会保持传入的具名切片类型而非退化为[]T。测试 find_test.go 验证了这一点:
type myStrings []string allStrings := myStrings{"a", "b", "a", "c", "b"} result, err := FindDuplicatesByErr(allStrings, func(s string) (string, error) { return s, nil }) is.NoError(err) is.IsType(result, allStrings, "type preserved") // 返回值仍是 myStrings 类型这在用类型别名携带业务语义(如type OrderIDs []int64)的项目中非常实用,无需手动做类型断言或转换。
与同类助手的选型建议
在 lo 的 find 子模块中,围绕"重复/唯一"有一组定位互补的助手(可参考文档目录 docs/data 下的core-findduplicates*.md、core-finduniques*.md):
- FindDuplicates(实现):元素本身可比较时直接判重,无需
iteratee; - FindDuplicatesBy(实现):按
iteratee生成的键判重,不可出错; - FindDuplicatesByErr(本文主角):按键判重,且键计算过程可出错并提前终止;
- FindUniques / FindUniquesBy(实现 / 实现):语义相反,返回只出现一次的唯一元素,其中小集合路径还专门设计了避免 map 分配的线性扫描实现(
findUniquesBySmall,见 find.go)。
选型建议:
- 键计算是纯函数 → 用
FindDuplicatesBy即可,性能更好、签名更简单; - 键计算可能失败(如解析用户输入、调用外部服务)→ 必须用
FindDuplicatesByErr,把错误显式传递出来; - 元素本身可比较 → 直接用
FindDuplicates,省去一层映射。
延伸阅读
- 完整实现:find.go(
FindDuplicatesByErr)、find.go(FindDuplicatesBy及其大小集合路径) - 测试用例:find_test.go(正常路径、错误中止路径、类型保持三组表驱动测试)
- 相关文档:core-findduplicatesby.md、core-findduplicates.md、core-finduniquesby.md
- find 子模块的完整能力清单可参阅 docs/docs/core/find.md,基准测试见 benchmark/core_find_bench_test.go(其中
BenchmarkFindDuplicatesBy使用v % 50作为键进行压测)
【免费下载链接】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),仅供参考