
lo 库 UniqBy 深度解析基于自定义 Key 的 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 库基于 Go 1.18 泛型的 Lodash 风格工具库的UniqBy函数展开完整讲解其函数签名、语义约定、典型用法并结合 slice.go 中的源码剖析其“小输入线性扫描 / 大输入哈希表”双路分发机制、UniqByErr错误变体以及配套测试的验证方式读完后可直接在项目中正确使用该函数并理解其底层取舍。函数定位与签名UniqBy是 lo 库core包切片slice分类下的去重函数定义于 slice.go 第 293 行文档数据文件为 docs/data/core-uniqby.md。其完整泛型签名如下func UniqBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice各类型参数的含义T any切片元素类型不做限制元素本身可以不可比较如 struct 切片U comparableiteratee计算出的 key 类型必须可比较因为 key 需要用于相等性判断底层放入 map 或做逐一遍历比较Slice ~[]T结果与输入同类型的切片约束命名类型如type MySlice []User可以原样透传返回类型与传入类型保持一致。语义上UniqBy返回切片的无重复版本对每个不同的 key 只保留首次出现的元素结果的顺序由元素在原切片中的出现顺序决定。iteratee会在每个元素上被调用用于生成唯一性判定的标准criterion——这正是它区别于普通值去重Uniq的核心去重依据不再是元素本身而是函数派生出的投影 key。README.md 中对它的描述与文档一致Returns a duplicate-free version of a slice, in which only the first occurrence of each element is kept. The order of result values is determined by the order they occur in the slice. It acceptsiterateewhich is invoked for each element in the slice to generate the criterion by which uniqueness is computed.基本用法文档给出的标准示例同时收录于 slice_test.go 的TestUniqBy_small中lo.UniqBy( []int{0, 1, 2, 3, 4, 5}, func(i int) int { return i % 3 }, ) // []int{0, 1, 2}逐步展开这个例子0, 1, 2的 key 分别是0, 1, 2而3, 4, 5的 key 又回到0, 1, 2因此后三个元素全部被判为重复并丢弃。这说明去重是按 key 而非按值进行的原值3与0完全不同但 key 相同故只保留先出现的0。一个更贴近业务的例子是按对象某字段去重iteratee返回字段值type User struct { ID int Name string } users : []User{{1, Alice}, {2, Bob}, {1, Charlie}} uniqueByID : lo.UniqBy(users, func(u User) int { return u.ID }) // []User{{1, Alice}, {2, Bob}} —— 同 ID 的 Charlie 被剔除保留首个 Alice同样的模式在 README.md 的IsUniqBy一节中也有对应演示func(u User) int { return u.ID }与func(u User) string { return u.Name }两种 key 的对照可见“字段投影作为唯一性标准”是该系列函数的通用心智模型。源码实现大小输入双路分发从源码看UniqBy并非单一的 map 实现而是按输入长度做了一次策略分发slice.gofunc UniqBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice { // The iteratee is invoked exactly once per element in both paths; see // uniqSmallInputThreshold for the map-vs-scan tradeoff. if len(collection) uniqSmallInputThreshold { return uniqBySmall(collection, iteratee) } return uniqByLarge(collection, iteratee) }分发阈值是一个包级常量slice.go// uniqSmallInputThreshold is the input-size cutoff below which Uniq/UniqBy skip // the seen map and rely on a linear scan of already-collected values/keys. For // tiny inputs the scan avoids the map allocation and per-element hashing; above // it the maps O(1) lookups win. Kept conservative (crossover is ~8-16). const uniqSmallInputThreshold 8即长度不超过 8 的切片走线性扫描路径更长的切片走 map 路径。注释中明确说明取 8 是一个保守的交叉点估计crossover is ~8-16小输入下省去 map 分配与逐元素哈希的开销更划算。大输入路径uniqByLarge哈希表func uniqByLarge[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice { result : make(Slice, 0, len(collection)) seen : make(map[U]struct{}, len(collection)) for i : range collection { key : iteratee(collection[i]) if _, ok : seen[key]; ok { continue } seen[key] struct{}{} result append(result, collection[i]) } return result }要点seen使用map[U]struct{}value 为零大小结构体只为记录“该 key 出现过”不额外占空间map 预分配容量为len(collection)result通过make(Slice, 0, len(collection))创建保留了输入的类型身份——Slice ~[]T约束在此体现传入命名类型切片时返回的仍是该命名类型测试中有is.IsType(nonempty, allStrings, type preserved)断言印证每个元素恰好调用一次iteratee重复元素同样会被计算 key只是结果被丢弃因此iteratee若带副作用或高昂开销调用次数等于切片长度而非结果长度。小输入路径uniqBySmall线性扫描func uniqBySmall[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice { result : make(Slice, 0, len(collection)) // keys mirrors result 1:1 so we can compare derived keys without re-invoking // the (possibly side-effecting) iteratee. keys : make([]U, 0, len(collection)) for i : range collection { key : iteratee(collection[i]) seen : false for j : range keys { if keys[j] key { seen true break } } if seen { continue } keys append(keys, key) result append(result, collection[i]) } return result }这里有一个值得注意的实现细节keys切片与result一一对应地镜像存储派生 key。源码注释解释了原因——比较时直接查keys避免对同一个元素第二次调用iteratee因为iteratee“可能是带副作用的”。这保证了无论走哪条路径语义完全一致iteratee严格每元素调用一次且基于同一批 key 值做判定。两条路径的时间复杂度可以推断为小路径最坏 O(n²)n ≤ 8代价可忽略大路径平均 O(n)。对使用者而言两者结果完全等价无需关心分发细节。错误变体UniqByErr当 key 的计算本身可能失败例如解析字符串、访问外部状态时应使用UniqByErrslice.gofunc UniqByErr[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) (U, error)) (Slice, error)行为约定由源码与测试共同确认iteratee返回第一个 error 时立即停止迭代函数返回(nil, err)已收集的部分结果被丢弃无错误时返回与UniqBy相同语义的去重结果。README.md 中的官方示例// Use UniqByErr when the iteratee function can return an error result, err : lo.UniqByErr([]int{0, 1, 2, 3, 4, 5}, func(i int) (int, error) { if i 3 { return 0, fmt.Errorf(number 3 is not allowed) } return i % 3, nil }) // []int(nil), error(number 3 is not allowed)slice_test.go 的TestUniqByErr用回调计数器严格验证了“出错即停”的短路语义错误发生在第 4 个元素时expectedCallbackCount: 4发生在第 1 个元素时为 1发生在最后元素时为 6——即回调次数精确等于出错位置的序号证明迭代确实在错误处终止。相关函数与配套测试文档元数据中声明了五个相似函数见 docs/data/core-uniqby.md 的similarHelpers它们构成一个去重函数族可按需求选取函数定位适用场景Uniq按元素值本身去重要求T comparable元素可直接比较时无需 iterateeUniqBy按自定义 key 去重保留首个本文主题最常用UniqByErr同UniqByiteratee 可返回错误key 计算可能失败IsUniq/IsUniqBy不修改数据只判定是否唯一校验而非去重nil/空切片返回 truePartitionBy按 key 分组而非去重需要保留每个 key 的全部元素测试覆盖方面slice_test.go 刻意把用例拆成两组以覆盖双路分发TestUniqBy_small所有输入长度 ≤ 8走uniqBySmall同时用命名类型type myStrings []string断言结果类型被保留is.IsTypeTestUniqBy_large12 元素的[]int{10, 20, 30, 20, 40, 50, 60, 70, 80, 90, 40, 10}按v/10去重得到[]int{10, 20, 30, 40, 50, 60, 70, 80, 90}并有 sanity check 断言输入确实超过阈值强制覆盖uniqByLarge分支。这种“按阈值拆分用例”的写法值得在其他带分支策略的函数测试中借鉴。此外lo 库的it包还提供了一个面向 Go 1.23 迭代器协议iter.Seq的序列版本it.UniqByit/seq.go返回一个惰性求值的序列函数而非物化切片func UniqByT any, U comparable, I ~func(func(T) bool) U) I其实现是闭包内维护seen : make(map[U]struct{})消费方通过yield提前终止时整个序列即返回。需要留意其文档注释指出的限制map 会分配可容纳所有不同 key 的空间长且异构distinct key 极多的序列可能造成过量内存占用且是 map-only 实现没有小输入扫描路径。处理有限内存敏感的场景时优先选择 core 包的lo.UniqBy。使用建议小结元素本身可比较且去重依据就是值用lo.Uniq少写一个 iteratee需要按字段/投影去重用lo.UniqBy记住“每个 key 保留首个、顺序与原切片一致、每个元素必调用一次 iteratee”三条语义iteratee 可能出错改用lo.UniqByErr并处理nil结果只是校验不修改用IsUniq/IsUniqBy迭代器管道中的一员去重用it.UniqBy注意其内存特性。以上行为均可在当前仓库中通过源码slice.go、it/seq.go与测试slice_test.go直接复核相关说明文档位于 docs/data/core-uniqby.md 与 docs/data/core-uniqbyerr.md。【免费下载链接】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),仅供参考