lo 库 Range 系列函数完全指南:从 Range 到 RangeFrom、RangeWithSteps 生成数值序列
【免费下载链接】lo💥 A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo
Range是 lo(一个基于 Go 1.18+ 泛型的 Lodash 风格 Go 库)中用于生成整数序列的核心数学工具函数,与RangeFrom、RangeWithSteps共同构成完整的数值序列生成能力,广泛用于切片初始化、循环计数、分页索引构造等场景。本文将结合源码实现与测试用例,深入讲解Range的正向/负向序列语义、底层分配机制,并扩展覆盖RangeFrom与RangeWithSteps的泛型用法,帮助读者在实际项目中正确、高效地生成任意数值序列。
Range:从 0 开始的定长整数序列
lo.Range是 lo 数学工具集(docs/docs/core/math.md)中的基础函数,其函数签名定义于 docs/data/core-range.md:
func Range(elementNum int) []int它创建一个指定长度的整数切片,从 0 开始递增;当传入负数长度时,生成递减序列:
lo.Range(4) // []int{0, 1, 2, 3} lo.Range(-4) // []int{0, -1, -2, -3}实现原理:符号决定步长
Range的实现在仓库根目录的 math.go 中,代码非常精简:
// Range creates a slice of numbers (positive and/or negative) with given length. func Range(elementNum int) []int { step := Ternary(elementNum < 0, -1, 1) length := elementNum * step result := make([]int, length) for i, j := 0, 0; i < length; i, j = i+1, j+step { result[i] = j } return result }其核心逻辑可以拆解为三步:
- 确定步长:通过
Ternary(elementNum < 0, -1, 1)判断元素个数的正负。elementNum为正时步长为+1,为负时步长为-1。Ternary本身也是 lo 提供的条件表达式辅助函数(实现于 condition.go)。 - 计算长度:
length := elementNum * step将负数取绝对值,得到切片实际容量。例如Range(-4)的length = (-4) × (-1) = 4。 - 填充序列:使用
for i, j := 0, 0; ...双变量循环,i跟踪索引、j跟随步长累加,一次性完成切片填充,避免了额外的 append 扩容开销。
边界情况:零长度的行为
当elementNum为 0 时,length为 0,make([]int, 0)会返回一个空切片。这在测试用例 math_test.go 中有明确验证:测试表包含三组用例——正数(4→{0,1,2,3})、负数(-4→{0,-1,-2,-3})以及零(0→ 空切片nil),零长度用例通过is.Empty(result)断言通过。
从测试结构看,Range及其系列函数在仓库中被视为一等公民:每个函数都有对应的独立测试函数(TestRange、TestRangeFrom、TestRangeWithSteps),且全部使用t.Parallel()并发执行,保证并发安全语义。
使用场景
Range最典型的应用是构造固定长度的索引切片,例如分页组件中的页号序列、初始化棋盘/矩阵的下标集合,或配合lo.Map生成占位数据:
// 生成 5 页的分页索引 [0,1,2,3,4] pages := lo.Range(5) // 配合 Map 生成业务数据 rows := lo.Map(lo.Range(10), func(i int, _ int) string { return fmt.Sprintf("row-%d", i) })负长度生成递减序列的特性,可以用于倒计时或反向遍历辅助:
countdown := lo.Range(-3) // []int{0, -1, -2}RangeFrom:指定起始值的定长序列
当需要从非零起始值生成定长序列时,使用RangeFrom。其签名定义于 docs/data/core-rangefrom.md:
func RangeFromT constraints.Integer | constraints.Float []T它创建一个从start开始、长度由elementNum决定的切片,负长度同样产生递减序列:
lo.RangeFrom(1, 5) // []int{1, 2, 3, 4, 5}泛型支持:整数与浮点通用
RangeFrom是泛型函数,类型参数T被约束为constraints.Integer | constraints.Float,该约束定义在 internal/constraints/constraints.go,覆盖int、int8、int64、uint、float32、float64等所有内置数值类型。实现位于 math.go:
func RangeFromT constraints.Integer | constraints.Float []T { step := Ternary(elementNum < 0, -1, 1) length := elementNum * step result := make([]T, length) for i, j := 0, start; i < length; i, j = i+1, j+T(step) { result[i] = j } return result }与Range的差异仅在两点:初始值j从start出发,以及步长通过T(step)显式转换为目标类型T。这保证了浮点类型(如float64)也能正确累加。
测试 math_test.go 同时验证了int与float64两种类型:RangeFrom(1, 5)得到升序序列,RangeFrom(-1, -5)得到降序序列{-1,-2,-3,-4,-5},RangeFrom(10, 0)返回空切片。
RangeWithSteps:指定起点、终点与步长的序列
当需要精确控制步长(而非固定 ±1)时,使用RangeWithSteps。其签名定义于 docs/data/core-rangewithsteps.md:
func RangeWithStepsT constraints.Integer | constraints.Float []T它创建一个从start开始、到end结束(不包含end)、按step递增或递减的切片:
lo.RangeWithSteps(0, 20, 5) // []int{0, 5, 10, 15}方向匹配与空结果规则
RangeWithSteps的行为规则比前两者更丰富,实现在 math.go:
start == end或step == 0:直接返回空切片[]T{}。步长为 0 会导致无限循环,因此被显式拦截。- 升序场景(
start < end):若step < 0(方向不匹配),返回空切片;否则从start循环累加step直到不小于end。 - 降序场景(
start > end):若step > 0(方向不匹配),返回空切片;否则从start循环累减直到不大于end。
因此以下调用都会返回空切片:
lo.RangeWithSteps(0, 5, 0) // step 为 0 lo.RangeWithSteps(0, 5, -1) // 升序却给负步长 lo.RangeWithSteps(5, 0, 1) // 降序却给正步长而合法降序用法为:
lo.RangeWithSteps(10, 0, -2) // []int{10, 8, 6, 4, 2}预分配容量的巧妙处理
值得注意的实现细节是 math.go 中的容量预分配逻辑:
capacity := func(count, delta T) int { // Use math.Ceil instead of (count-1)/delta+1 because integer division // fails for floats (e.g., 5.5/2.5=2.2 → ceil=3, not 2). return int(math.Ceil(float64(count) / float64(delta))) }代码注释明确解释了选择math.Ceil而非(count-1)/delta+1的原因:后者的整数除法在浮点场景下会出错(例如5.5/2.5=2.2,向下取整得到 2,而实际需要 3 个元素)。这个细节体现了该函数对浮点类型支持的严谨性,也提示读者:当T为浮点类型时,RangeWithSteps返回的元素个数可能与直觉略有出入,建议在关键业务中先验证结果长度。
it 包变体:Range 的惰性序列版本
仓库还提供基于 Go 1.23iter.Seq的迭代器版本(it/math.go),位于it包,文件名含//go:build go1.23构建标签,仅在 Go 1.23+ 环境下生效:
// Range creates a sequence of numbers (positive and/or negative) with given length. func Range(elementNum int) iter.Seq[int] { step := lo.Ternary(elementNum < 0, -1, 1) length := elementNum * step return func(yield func(int) bool) { for i, j := 0, 0; i < length; i, j = i+1, j+step { if !yield(j) { return } } } }该版本与切片版共享相同的步长/长度计算逻辑(对应文档 docs/data/it-range.md),区别在于:
- 惰性求值:序列只在被消费时按需生成,适合构造大范围序列但只取前几个元素的场景;
- 提前终止:
yield返回false时立即停止迭代,天然支持break语义; - 不产生堆分配:无需
make切片,内存开销更小。
it包同样提供RangeFrom与RangeWithSteps的迭代器版本(分别见 docs/data/it-rangefrom.md 与 docs/data/it-rangewithsteps.md),方向匹配规则与切片版完全一致。
性能特征与基准测试
仓库的基准测试 benchmark/core_math_bench_test.go 为三个函数分别编写了BenchmarkRange、BenchmarkRangeFrom、BenchmarkRangeWithSteps,均以lengths中的多组数据规模压测。从实现结构看,Range与RangeFrom使用make一次性预分配(一次分配、零扩容),而RangeWithSteps也通过capacity闭包精确预分配,因此三个函数在正常参数下都是单次分配的最优路径。
基准测试还按数据规模分b.Run(strconv.Itoa(n))子测试,便于观察不同长度下的分配与填充耗时。需要说明的是:本文不引用具体基准数据,读者如需对比可自行在本地运行go test -bench=BenchmarkRange -benchmem(基准测试目录位于 benchmark/)。
相关辅助函数
Range系列在文档体系中与以下函数互为关联(见 docs/data/core-range.md 的similarHelpers字段):
lo.Times:重复调用回调指定次数并收集结果,可视为「生成序列 + Map」的合成操作;lo.Repeat/lo.RepeatBy:按次数重复元素或按回调重复生成(docs/data/core-repeat.md、docs/data/core-repeatby.md)。
实际编码时可按需组合:例如先用lo.Range生成索引序列,再叠加lo.Map、lo.Filter等函数完成流水线式处理。
快速上手
在 Go 1.18+ 项目中安装 lo 并导入即可使用(安装命令见 README.md):
go get github.com/samber/lo@v1import "github.com/samber/lo" // 升序定长序列 lo.Range(4) // []int{0, 1, 2, 3} // 降序定长序列 lo.Range(-4) // []int{0, -1, -2, -3} // 指定起点的定长序列(支持整数与浮点) lo.RangeFrom(1, 5) // []int{1, 2, 3, 4, 5} // 指定起点、终点与步长(不含终点) lo.RangeWithSteps(0, 20, 5) // []int{0, 5, 10, 15}若项目已升级至 Go 1.23,还可使用it包的惰性序列版本处理大范围数据,兼顾内存效率与中断语义。
【免费下载链接】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),仅供参考