
lo 库 it 包 Filter 深度解析Go 1.23 Range 序列上的惰性过滤操作【免费下载链接】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 风格工具库中it包面向 Go 1.23 引入的 Range over funciter.Seq序列提供了一组惰性操作符其中Filter/FilterI是最基础的筛选原语。读完本文你将掌握如何在 Range 序列上进行惰性过滤、理解其不提前分配、支持中途终止的实现机制并知道它与核心包lo.Filterslice 版及Reject、FilterMap、TakeFilter等相邻 helper 的边界与选型依据。一、it 包的适用前提Go 1.23 的 Range over funcdocs/data/it-filter.md 的 frontmatter 将本 helper 归类于iter / sequence类别源码定位在it/seq.go文档标注sourceRef: it/seq.go#L34。it包的所有文件如 it/seq.go首行均带有构建约束//go:build go1.23这意味着整个it包只能在Go 1.23 及以上版本编译依赖标准库iter包提供的iter.Seq[T]即~func(func(T) bool)类型的 yield 函数而 go.mod 本身声明的是go 1.18说明 lo 模块的主体lo核心包、mutable、parallel等仍支持 Go 1.18it包是面向新版语言特性的增量扩展因此使用it.Filter的前提是编译工具链为 Go 1.23且输入输出都是可迭代的 Range 序列而不是 slice。输入序列的典型来源包括标准库的slices.Values、自定义的func(yield func(T) bool)闭包以及it包自己的生成器如it.Range、it.Times等。二、两个函数签名Filter 与 FilterI文档给出的两个签名与 it/seq.go#L34-L50 中的实现完全一致// it/seq.go func FilterT any, I ~func(func(T) bool) bool) I func FilterIT any, I ~func(func(T) bool) bool) I逐段拆解部分含义T any元素类型无约束任何类型均可包括不可比较类型I ~func(func(T) bool)类型参数I是底层为func(func(T) bool)的函数类型或其命名类型注意~是底层类型匹配允许自定义命名序列类型predicate func(item T) bool无索引版本的判定函数只看元素本身predicate func(item T, index int) boolFilterI的判定函数额外接收元素在序列中的 0 基索引返回值I返回与输入同型的新序列——输入什么序列类型输出就是什么序列类型Filter是FilterI的便捷包装it/seq.go#L34-L36func FilterT any, I ~func(func(T) bool) bool) I { return FilterI(collection, func(item T, _ int) bool { return predicate(item) }) }也就是说所有过滤逻辑集中在FilterI中Filter只是丢弃索引的适配层。这一设计在it包中是通用模式Map/MapI、Reject/RejectI、UniqMap/UniqMapI同理。三、Filter 基本用法过滤出满足条件的元素文档中的第一个示例——从 1~5 的序列中筛出偶数docs/data/it-filter.md 的### Filter小节seq : func(yield func(int) bool) { _ yield(1) _ yield(2) _ yield(3) _ yield(4) _ yield(5) } filtered : it.Filter(seq, func(x int) bool { return x%2 0 }) var result []int for v : range filtered { result append(result, v) } // result contains 2, 4 (even numbers)要点说明输入是惰性序列seq只是声明了如何产生 1~5在it.Filter被调用时没有任何元素被实际产出返回值同样是惰性序列filtered调用瞬间不执行任何过滤只有当下方for v : range filtered开始消费时才逐个拉取、逐个判定结果顺序保持产出顺序与输入序列顺序一致此处为2, 4。仓库的 it/seq_example_test.go 提供了等价的官方示例且演示了用slices.Values直接以 slice 作为输入序列func ExampleFilter() { // it/seq_example_test.go list : slices.Values([]int64{1, 2, 3, 4}) result : Filter(list, func(nbr int64) bool { return nbr%2 0 }) fmt.Printf(%v, slices.Collect(result)) // Output: [2 4] }注意slices.Collect(result)将惰性序列物化为 slice 是最常用的落地方式与文档中手写for range append等价但更简洁。四、FilterI带索引的过滤当判定条件依赖元素位置时例如跳过前两个之后的偶数使用FilterI。文档中的第二个示例result : it.FilterI(it.Range(1, 6), func(item int, index int) bool { return item%2 0 index 1 }) var filtered []int for v : range result { filtered append(filtered, v) } // filtered contains [4, 6]推演过程it.Range(1, 6)产出1, 2, 3, 4, 5indexitemitem%2 0index 1结果01falsefalse排除12truefalse排除23falsetrue排除34truetrue保留45falsetrue排除最终得到[4, 6]此处原文档注释的[4, 6]对应it.Range的半开区间约定若实际序列为1~5则保留4。索引index是元素在输入序列中的位置0 基而非输出序列中的位置——这一点从源码实现即可确认见下节。五、源码实现惰性、索引语义与早停机制FilterI 的完整实现it/seq.go#L40-L50 只有 11 行值得逐行剖析func FilterIT any, I ~func(func(T) bool) bool) I { return func(yield func(T) bool) { var i int for item : range collection { if predicate(item, i) !yield(item) { return } i } } }1. 惰性求值lazy函数体返回的是一个闭包而非结果集合。predicate与collection只在该闭包首次被消费外层for range拉取时才生效。因此没有为整个结果分配 slice 或 map内存占用是 O(1) 的游标状态仅一个i int计数器如果消费方只取前几个元素就停止collection也只被拉取到那个位置后续元素永远不会被产出——这要求输入序列对提前放弃是安全的iter.Seq协议允许消费方随时停止迭代。2. 索引语义i从 0 开始每从collection取到一个元素就加 1与元素是否被 predicate 保留无关。也就是说index 是输入序列中的位置。这解释了第四节中为什么2第一个偶数会因index 1被index 1排除。3. 早停early termination协议if predicate(item, i) !yield(item) { return }这行实现了iter.Seq的中止约定yield(item)返回false表示消费方for range已经break此时必须停止拉取上游若 predicate 为false该元素被静默丢弃不触发yield也不影响计数。仓库的测试 it/seq_test.go#L87-L118 专门验证了这三点行为包括assertSeqSupportBreak(t, r1)这个断言序列支持消费方提前中断func TestFilterI(t *testing.T) { t.Parallel() t.Run(int evens, func(t *testing.T) { r1 : FilterI(values(1, 2, 3, 4), func(x, _ int) bool { return x%2 0 }) assertSeqSupportBreak(t, r1) // 序列支持 break 提前终止 is.Equal([]int{2, 4}, slices.Collect(r1)) // 过滤结果 }) t.Run(type preserved, func(t *testing.T) { type myStrings iter.Seq[string] // 自定义命名序列类型 allStrings : myStrings(values(, foo, bar)) nonempty : FilterI(allStrings, func(x string, _ int) bool { return len(x) 0 }) is.IsType(nonempty, allStrings, type preserved) // 返回类型保持为 myStrings }) }4. 类型保持type preserved签名中I ~func(func(T) bool)的~通配让Filter接受任意以func(func(T) bool)为底层类型的命名类型并且原样返回同一类型。测试中的myStrings iter.Seq[string]经Filter处理后仍断言为myStrings类型。这让自定义管道例如给序列附加方法在过滤后可以继续链式使用而不会退化成匿名的iter.Seq。这一点也是它与许多手写包装器相比的设计优势从源码结构看it包中所有接收I的 helperUniq、Drop、Take、Reverse、Shuffle等都采用了同一约束风格。六、Filter 在 it 包内部的应用它是许多 helper 的基石从源码看FilterI并不只是一个对外 API它还是it包内部其他序列 helper 的构建块Dropit/seq.go#L628-L638丢弃前 n 个元素直接实现为FilterI(collection, func(_ T, index int) bool { return index n })正是用索引过滤的标准用法Compactit/seq.go#L933-L935过滤出所有非零值元素实现为Filter(collection, lo.IsNotEmpty)DropByIndexit/seq.go#L745-L748按指定索引集合丢弃元素通过RejectI即反向 FilterIlo.HasKey实现TakeFilterit/seq.go#L753-L781文档注释明确写道Equivalent to calling Take(Filter(...))——先过滤再截取前 n 个匹配项但它单独实现了循环以便在凑够 n 个匹配后立刻停止比Take(Filter(...))更高效。这些组合提示了一个实践结论当你需要过滤 截断这类复合语义时优先查it包是否已有融合版 helper如TakeFilter否则再手动组合Filter与其他操作符。由于一切都是惰性序列链式组合Take(FilterI(src, ...), n)不会产生中间 slice。七、与核心包 lo.Filterslice 版及相邻 helper 的对比文档 frontmatter 中similarHelpers指向了核心包的core#slice#filter、core#slice#filteri以及iter#sequence#reject。三者定位不同核心包 lo.Filter立即求值的 slice 操作lo.Filterslice.go#L12-L22func Filter[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) bool) Slice { result : make(Slice, 0, len(collection)) for i : range collection { if predicate(collection[i], i) { result append(result, collection[i]) } } return result }对比要点维度it.Filter/it.FilterIlo.Filter核心包输入Range 序列I ~func(func(T) bool)sliceSlice ~[]T输出同型惰性序列按需产出立即分配并填充的 slice索引参数Filter无索引FilterI有索引两个独立函数单一函数即带(item, index)适用工具链Go 1.23Go 1.18模块基线消费方早停支持上游随之停止不适用结果已物化另外核心包还有错误容忍变体 lo.FilterErrslice.go#L27-L41predicate 返回(bool, error)出错立即停止而it包的Filter没有 Err 变体——从源码结构看it包目前未提供FilterErr序列版需要错误传播的过滤场景应留在 slice 侧使用lo.FilterErr或自行用iter惯用法包装。it.RejectFilter 的补集it/seq.go#L785-L801 的Reject/RejectI逻辑与FilterI完全对称只是条件取反if !predicate(item, i) !yield(item)。选型建议predicate 语义天然是保留条件时用Filter天然是排除条件如剔除脏数据时用Reject可读性更好。FilterMap过滤与变换的一步完成it/seq.go#L94-L114 的FilterMap/FilterMapI让回调一次返回(R, bool)——变换结果加保留判定避免先 Filter 再 Map两次遍历序列的开销func FilterMapT, R any (R, bool)) iter.Seq[R]如果过滤与类型变换强耦合例如解析字符串成功则保留解析结果优先使用FilterMap而不是Map之后再Filter。组合示例在 it 包中串联常用操作符综合以上 helper一个典型的 Range 序列管道各步骤均惰性无中间分配// 从 1..100 中取偶数、丢弃前 3 个、只保留前 5 个全部惰性执行 evens : it.Filter(it.Range(1, 101), func(n int) bool { return n%2 0 }) trimmed : it.Drop(evens, 3) // 等价于 FilterI(evens, func(_ int, i int) bool { return i 3 }) top5 : it.Take(trimmed, 5) for v : range top5 { _ v }八、实践要点与常见坑predicate 中不要有跨元素状态假设过滤是逐元素流式判定it包的过滤 helper 不提供回看/前瞻能力需要窗口或跨元素条件时应改用it.Window/it.Sliding等产生[]T的 helper 后再过滤。序列可被多次消费但每次重新求值Filter返回的闭包持有对输入序列的引用for range多次迭代会重新遍历上游若上游是一次性来源如 channel 派生的序列注意不要重复消费。it包提供Drain等工具用于显式消费。索引是输入序列的索引FilterI的index不因丢弃而重排源码中i在每次拉取后自增见 it/seq.go#L42-L48若需要保留后的第 k 个应消费结果序列后自行计数或使用TakeFilter。Go 版本it包文件带//go:build go1.23约束低于 1.23 的工具链下it包整体不参与编译请改用核心包lo.Filterslice 版处理已就位的切片数据。与测试对齐本文所有行为结论均可在 it/seq_test.go 的TestFilter/TestFilterIL55–L118与 it/seq_example_test.go 的ExampleFilter中复现验证运行go test ./it/Go 1.23即可复跑。小结it.Filter/it.FilterI是 lo 库在 Go 1.23 Range over func 模型下的惰性过滤原语以I ~func(func(T) bool)的类型约束实现同型进出与命名类型保持以 11 行闭包实现零分配的流式过滤并通过yield返回值协议支持消费方早停。它是Drop、Compact等 helper 的底层基石也是与核心包 slice 版lo.Filter相互补充的序列侧对应物。理解了它的惰性、索引与早停语义后就可以放心地将其与Map、Take、Reject、FilterMap等组合成无中间物化的完整处理管道。【免费下载链接】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),仅供参考