lo 库 FindDuplicatesByErr 详解:基于 iteratee 键值查找重复元素并支持错误中止
2026/9/13 12:35:01 网站建设 项目流程

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(文档)是"错误处理变体"关系:FindDuplicatesByiteratee只返回键,而FindDuplicatesByErriteratee额外返回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>

执行过程逐步拆解:

  1. 依次计算键:3 % 3 = 04 % 3 = 15 % 3 = 26 % 3 = 07 % 3 = 1
  2. 01各出现两次,属于重复键;键2只出现一次;
  3. 结果取每个重复键在集合中首次出现的元素:键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"。

测试同时断言出错时返回值为nilis.Nil(result, "nil should be returned on error"))。

源码原理:两趟扫描 + map 状态机

FindDuplicatesByErr的实现(find.go)采用"统计 + 收集"两趟扫描,与FindDuplicatesByfindDuplicatesByLarge路径共享同一套 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 的输出完全一致。二者的定位差异可总结为:

对比项FindDuplicatesByFindDuplicatesByErr
iteratee 签名func(item T) Ufunc(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*.mdcore-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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询