用 go-str2duration 在 Go 中解析带"天/周"的时间字符串:Loki 仓库 v2 实现全解
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
本文围绕 Loki 仓库中 vendored 的github.com/xhit/go-str2duration/v2依赖(版本 v2.1.0)展开,讲解如何将1w2d6h3ns、2d3s96ns这类人类可读的字符串安全解析为time.Duration,并反向格式化为紧凑字符串。读完本文,你将掌握该库的全部单位规则、ParseDuration与String的完整用法、底层解析算法,以及它在标准库time.ParseDuration之上的扩展能力。
一、这是什么库:比 time.ParseDuration 多出 d 和 w
Go 标准库的time.ParseDuration只能识别ns、us/µs、ms、s、m、h六种单位,遇到"1 周 2 天"这种自然语言式的持续时间只能先手动换算成小时。go-str2duration/v2在完全兼容time.Duration.String()输出格式的基础上,额外支持d(天)和w(周)两个单位,并把us与µs都视为微秒。
按该库 README 的定义(见 vendor/github.com/xhit/go-str2duration/v2/README.md),它支持三类字符串的转换:
time.Duration.String()能产生的所有字符串,例如1h、4.000000001s、1h1m0.01s;- 更易读的连续单位字符串,例如
1w2d6h3ns(1 周 2 天 6 小时 3 纳秒); - 微秒的两种写法
µs和us等价。
一个明确的约定:1 天 = 24 小时(不区分日历天与自然日)。如果业务不需要天和周,官方建议直接用标准库time.ParseDuration。
二、下载与引入
在 Go 模块项目中直接获取(当前 Loki 仓库通过 vendor 目录固定了v2.1.0,见 go.mod):
go get github.com/xhit/go-str2duration/v2在代码中引入,注意包路径末尾的/v2:
import str2duration "github.com/xhit/go-str2duration/v2"仓库事实:Loki 的 go.mod 将该依赖声明为
// indirect,go.sum 记录了v2.1.0的哈希校验,源码位于 vendor/github.com/xhit/go-str2duration/v2/str2duration.go。
三、ParseDuration:解析入口与完整示例
核心 API 只有一个:
func ParseDuration(s string) (time.Duration, error)下面这段取自 README 的完整示例,覆盖了全部单位组合、小数、负数语义与混合写法,可直接复制运行验证:
package main import ( "fmt" str2duration "github.com/xhit/go-str2duration/v2" "time" ) func main() { for i, tt := range []struct { dur string expected time.Duration }{ // 这是 time.Duration.String() 会产生的字符串 {"1h", time.Duration(time.Hour)}, {"1m", time.Duration(time.Minute)}, {"1s", time.Duration(time.Second)}, {"1ms", time.Duration(time.Millisecond)}, {"1µs", time.Duration(time.Microsecond)}, {"1us", time.Duration(time.Microsecond)}, {"1ns", time.Duration(time.Nanosecond)}, {"4.000000001s", time.Duration(4*time.Second + time.Nanosecond)}, {"1h0m4.000000001s", time.Duration(time.Hour + 4*time.Second + time.Nanosecond)}, {"1h1m0.01s", time.Duration(61*time.Minute + 10*time.Millisecond)}, {"1h1m0.123456789s", time.Duration(61*time.Minute + 123456789*time.Nanosecond)}, {"1.00002ms", time.Duration(time.Millisecond + 20*time.Nanosecond)}, {"1.00000002s", time.Duration(time.Second + 20*time.Nanosecond)}, {"693ns", time.Duration(693 * time.Nanosecond)}, // 这些不是 time.Duration.String() 的输出,但同样可读可解析 {"1ms1ns", time.Duration(time.Millisecond + 1*time.Nanosecond)}, {"1s20ns", time.Duration(time.Second + 20*time.Nanosecond)}, {"60h8ms", time.Duration(60*time.Hour + 8*time.Millisecond)}, {"96h63s", time.Duration(96*time.Hour + 63*time.Second)}, // 支持天和周! {"2d3s96ns", time.Duration(48*time.Hour + 3*time.Second + 96*time.Nanosecond)}, {"1w2d3s96ns", time.Duration(168*time.Hour + 48*time.Hour + 3*time.Second + 96*time.Nanosecond)}, {"10s1us693ns", time.Duration(10*time.Second + time.Microsecond + 693*time.Nanosecond)}, } { durationFromString, err := str2duration.ParseDuration(tt.dur) if err != nil { panic(err) } else if tt.expected != durationFromString { fmt.Println(fmt.Sprintf("index %d -> in: %s returned: %s\tnot equal to %s", i, tt.dur, durationFromString.String(), tt.expected.String())) } else { fmt.Println(fmt.Sprintf("index %d -> in: %s parsed succesfully", i, tt.dur)) } } }从示例可以提炼出的关键语义:
- 单位可按任意顺序连续书写,各段数值独立累加,如
10s1us693ns; - 同单位可以重复出现多次,解析器按"段"逐一累加,例如
96h63s并不会报错,最终等于96*time.Hour + 63*time.Second; - 小数只作用于紧跟其后的一段单位,如
4.000000001s精确到纳秒; µs(U+00B5 微符号)与us等价,都表示微秒。
四、源码级原理:unitMap 与三段式解析循环
4.1 单位表
unitMap 定义了全部单位与其纳秒基数:
| 单位 | 含义 | 纳秒值 |
|---|---|---|
ns | 纳秒 | time.Nanosecond |
us/µs/μs | 微秒(两种 Unicode 写法均支持) | time.Microsecond |
ms | 毫秒 | time.Millisecond |
s | 秒 | time.Second |
m | 分 | time.Minute |
h | 时 | time.Hour |
d | 天(24 小时) | time.Hour * 24 |
w | 周(7 天) | time.Hour * 168 |
注意源码里微秒实际接受三种写法:us、µs(U+00B5)以及μs(U+03BC 希腊字母 mu),README 中只强调了前两者。
4.2 解析主循环
ParseDuration 的算法严格遵循正则骨架[-+]?([0-9]*(\.[0-9]*)?[a-z]+)+,对每个"数值+单位"段重复四步:
- 符号处理:若首字符为
-或+,记录负数标志并跳过;单独的"0"直接返回零值; - 整数部分:由 leadingInt 逐位消费
[0-9]*,并在累乘超过(1<<63-1)/10时判定溢出; - 小数部分:遇到
.后由 leadingFraction 消费[0-9]*,同时维护scale放大系数;该函数对小数位溢出采取"放弃继续累加精度"的宽容策略,避免返回错误; - 单位消费:连续读取字母直到遇到数字或小数点,查
unitMap得到基数;若查不到则报unknown unit错误。
段内数值换算采用v += int64(float64(f) * (float64(unit) / scale)),源码注释点明:必须借助 float64 才能对小时的分数做到纳秒级精度(因为h是最大单位,f*unit/scale上限约3.6e+12ns,处于 float64 精确表示范围内)。
4.3 错误处理
与标准库一致,所有失败路径都返回非 nil error,典型错误信息包括:
time: invalid duration "..."—— 语法非法、缺少数字(如.s)、或累加溢出;time: missing unit in duration "..."—— 纯数字没有单位后缀;time: unknown unit "x" in duration "..."—— 单位不在unitMap中。
因此调用方务必像 README 示例那样检查err,而不是盲信输入。
五、String:反向格式化为紧凑字符串
除了解析,该库还提供反向转换:
func String(t time.Duration) string它生成1w4d2h3m5s形式的紧凑表示,具备两个优于标准库time.Duration.String()的特点:
- 输出天与周,而不是把 168 小时原样展开;
- 零值单位直接省略,例如
1d1ms表示"1 天 1 毫秒",不会出现0h0m之类的冗余片段。
从 String 实现 看,它采用从纳秒逐级上取余(ns→µs→ms→s→m→h→d→w)的固定缓冲算法:先处理纳秒位,依次除以 1000、1000、1000、60、60、24、7,每级仅在余数非零时写入数字与单位字符,最后统一处理负号;d == 0时特判返回"0s"。源码注释还给出该格式理论上能表示的最大值:15250w1d23h47m16s854ms775us807ns。
六、注意事项与适用边界
- 1 天固定 24 小时:该库不做日历换算,
1d恒等于24h,跨夏令时等场景需自行折算; - 不要与 time.ParseDuration 混淆:若输入只会出现在
time.Duration.String()输出中,直接使用标准库即可,二者对同一合法字符串的解析结果一致(go-str2duration 是标准库实现的超集); - 溢出防护:整数部分与最终累加都有
int64溢出检查,异常输入返回 error 而非静默截断; - 版本固定:当前 Loki 仓库以 vendor 方式锁定
v2.1.0,阅读源码时应以 vendor/github.com/xhit/go-str2duration/v2/str2duration.go 为准。
七、在 Loki 项目中的角色
在 Loki 仓库中,github.com/xhit/go-str2duration/v2是作为间接依赖被引入的:它在 go.mod 中以// indirect标记出现,并由go.sum(go.sum)与 vendor 目录共同固定版本。这意味着它的能力通过依赖链服务于 Loki 的构建,而非被 Loki 主代码直接import。对关注依赖安全与供应链的读者而言,这也是一个观察"vendor 目录如何锁定传递依赖版本"的典型样本。
结合其能力看,这类"支持天/周的人类可读时长"库在日志系统场景中天然适合解析用户配置里的保留期、告警窗口等时长参数——例如 Loki 的 retention 与超时类配置常以小时计,若扩展到周/天粒度,go-str2duration正是标准库之外的轻量替代品。
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考