Grafana Tempo 中的 go-humanize 工具库:让字节、时间与数字输出更人性化
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
导读
github.com/dustin/go-humanize是一个专注于把"机器友好的原始数值"转换为"人类可读字符串"的 Go 工具库:把82854982变成83 MB,把time.Time变成7 hours ago,把1000000变成1,000,000。Grafana Tempo 在 go.mod 中将其以 v1.0.1 版本引入并以 vendor 方式固化在仓库中(见 vendor/github.com/dustin/go-humanize),用于在日志、测试与输出场景中把内部数值翻译成直观表达。读完本文,你将掌握该库全部核心 API 的用法、输出规则与底层实现原理,并能在自己的 Go 项目中直接复用这些能力。
一、库概览:一行go get就能引入的纯标准库工具
go-humanize 的定位非常轻量:整个核心包只依赖 Go 标准库(fmt、strconv、math、sort、time、regexp等),没有任何外部运行时依赖,符合 Tempo "minimal dependency" 的设计取向。引入方式在 README.markdown 中说明:
import "github.com/dustin/go-humanize"使用时以humanize作为包名即可。它提供的功能按主题可分为六大类:
- Sizes:字节数的人性化展示与反向解析;
- Times:相对时间表达("3 天前");
- Ordinals:序数词(1st / 2nd / 3rd);
- Commas:千分位分组;
- Ftoa:去除尾随零的浮点格式化;
- SI notation:国际单位制前缀(k/M/G/n 等)。
另有humanize/english子包处理英文复数与词列连接。全部函数都在包的 humanize.go 与各主题源码文件中给出实现,下文逐一展开。
二、Sizes:字节数的双轨制格式化(SI 与 IEC)
这是整个库中使用频率最高的功能。核心目标是:把类似82854982的裸字节数,转换成用户一眼能读懂的83 MB或79 MiB。
2.1 两个入口:Bytes与IBytes
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.Bytes(s uint64)使用SI 单位制,以 1000 为底,后缀序列为B / kB / MB / GB / TB / PB / EB;IBytes(s uint64)使用IEC 单位制(二进制倍数),以 1024 为底,后缀序列为B / KiB / MiB / GiB / TiB / PiB / EiB。
两者共享同一个底层算法humanateBytes(见 bytes.go):
func humanateBytes(s uint64, base float64, sizes []string) string { if s < 10 { return fmt.Sprintf("%d B", s) } e := math.Floor(logn(float64(s), base)) suffix := sizes[int(e)] val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10 f := "%.0f %s" if val < 10 { f = "%.1f %s" } return fmt.Sprintf(f, val, suffix) }实现要点:
- 指数
e通过对数log_base(s)取整得到,从而确定落在哪一级后缀; - 结果值保留1 位小数(四舍五入到十分位),当换算后的数值 ≥ 10 时退化为整数展示;
- 小于 10 字节的数字直接原样输出,不做任何换算。
2.2 单位常量与反向解析ParseBytes
源码中定义了两套单位常量(bytes.go):
- IEC:
Byte = 1 << (iota * 10),依次得到KiByte / MiByte / GiByte / TiByte / PiByte / EiByte(每级 ×1024); - SI:
KByte = IByte * 1000,依次得到MByte / GByte / TByte / PByte / EByte(每级 ×1000)。
与正向格式化对应,ParseBytes能把字符串解析回字节数(bytes.go),支持数字部分含.与,(逗号会被剥离),单位部分不区分大小写,并支持无后缀的简写形式(如k、m、ki、mi)。示例:
humanize.ParseBytes("42 MB") // -> 42000000, nil humanize.ParseBytes("42 mib") // -> 44040192, nil若后缀无法识别(不在bytesSizeTable中)会返回unhandled size name: ...错误;若换算结果超过math.MaxUint64会返回too large错误。
2.3 大数据量版本bigbytes.go
对于超出uint64范围的超大数值,同目录下的 bigbytes.go 提供了基于math/big的BigBytes/BigIBytes/ParseBigBytes,思路与上面一致,只是底层换成大整数运算。
三、Times:把time.Time变成"多久之前 / 多久之后"
3.1 最简用法Time
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.Time(then time.Time)内部等价于RelTime(then, time.Now(), "ago", "from now")(见 times.go):过去的时间点后缀ago,未来的时间点后缀from now。
3.2 相对时间算法:RelTime与CustomRelTime
RelTime(a, b, albl, blbl):比较两个时间点,时间在前者使用albl标签,在后者使用blbl标签;CustomRelTime(a, b, albl, blbl, magnitudes):允许传入自定义的分档表。
核心算法(times.go):
- 计算
diff = b.Sub(a),若a晚于b则交换方向并切换标签; - 用
sort.Search在分档表magnitudes中二分查找第一个D > diff的档位; - 按该档位的
Format模板填充:%s替换为方向标签,%d替换为diff / DivBy。
3.3 默认分档表(可直接参照的展示规则)
默认档位定义在 times.go,这里完整列出:
| 时间差范围 | 输出格式 | 换算除数 |
|---|---|---|
| < 1 秒 | now | — |
| < 2 秒 | 1 second %s | — |
| < 1 分钟 | %d seconds %s | 秒 |
| < 2 分钟 | 1 minute %s | — |
| < 1 小时 | %d minutes %s | 分钟 |
| < 2 小时 | 1 hour %s | — |
| < 1 天 | %d hours %s | 小时 |
| < 2 天 | 1 day %s | — |
| < 1 周 | %d days %s | 天 |
| < 2 周 | 1 week %s | — |
| < 1 月(30 天) | %d weeks %s | 周 |
| < 2 月 | 1 month %s | — |
| < 1 年 | %d months %s | 月 |
| < 18 月 | 1 year %s | — |
| < 2 年 | 2 years %s | — |
| < 37 年 | %d years %s | 年 |
| 更长 | a long while %s | — |
其中Day = 24h、Week = 7 * Day、Month = 30 * Day、Year = 12 * Month、LongTime = 37 * Year都在 times.go 中定义。可以看出该库对"1 个单位"(如 1 second、1 minute、1 day)使用单数固定模板,大于 1 个单位的差值才进入"多单位 + 数字"模板,这正是它输出自然度的来源。
四、Ordinals:序数词输出
序数功能源自 Go 邮件列表的一次讨论,用于把整数变成序数表达:
0 -> 0th 1 -> 1st 2 -> 2nd 3 -> 3rd 4 -> 4th用法(README.markdown):
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.实现(ordinals.go)只依赖strconv,规则符合英语习惯:默认后缀th,仅当个位数为 1/2/3 且不是11/12/13(即x%100不为 11/12/13)时才改用st/nd/rd。
五、Commas:千分位分组
5.1Comma(int64)
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.行为示例:0 → 0、100 → 100、1000 → 1,000、1000000000 → 1,000,000,000、-100000 → -100,000。
实现要点(comma.go):
- 负号单独提取,负数绝对值参与分组;
- 从低位起每 3 位切一段,不足 3 位时左侧补
0再拼接; - 特殊处理
math.MinInt64:因为MinInt64无法被安全取负,直接返回硬编码字符串-9,223,372,036,854,775,808。
5.2Commaf与CommafWithDigits(float64)
humanize.Commaf(834142.32) // 834,142.32 humanize.CommafWithDigits(834142.32, 1) // 834,142.3Commaf(comma.go)先按strconv.FormatFloat(v, 'f', -1, 64)转字符串再拆分整数部分分组,小数部分原样保留;CommafWithDigits进一步用stripTrailingDigits限制小数位数。
5.3BigComma(big.Int)
对math/big大整数做同样分组的BigComma(comma.go),适合超大数值展示。
六、Ftoa:去掉尾随零的浮点格式化
Go 自带的%f会固定输出 6 位小数,产生2.240000、2.000000这种"机器味"很重的表达,go-humanize 用Ftoa解决:
fmt.Printf("%f", 2.24) // 2.240000 fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24 fmt.Printf("%f", 2.0) // 2.000000 fmt.Printf("%s", humanize.Ftoa(2.0)) // 2实现(ftoa.go):先按'f'格式输出 6 位小数,再通过stripTrailingZeros从右向左删除尾随0,遇到小数点也一并删掉,最终得到无尾随零的紧凑字符串。配套的FtoaWithDigits(num, digits)则先把结果截断到指定小数位数再去零,例如FtoaWithDigits(3.14159, 2)得到3.14。
七、SI notation:科学计数的人类友好化
7.1 正向输出SI/SIWithDigits
humanize.SI(0.00000000223, "M") // 2.23 nMSI(input, unit)会把任意数量级的值换算到最合适的 SI 前缀上,前缀取自完整的国际单位制表(si.go),覆盖从q(quecto,10⁻³⁰)到Q(quetta,10³⁰)的 21 个量级,其中包含:
- 小量级:
n(nano,10⁻⁹)、µ(micro,10⁻⁶)、m(milli,10⁻³); - 无量级:
""(10⁰); - 大量级:
k(kilo,10³)、M(mega,10⁶)、G(giga,10⁹)、T(tera,10¹²)等。
数值部分复用Ftoa去除尾随零,因此SI(1000000, "B")输出1 MB。SIWithDigits(input, decimals, unit)可额外限制小数位数。
7.2 计算层ComputeSI
ComputeSI(input) (float64, string)(si.go)返回"调整后的数值 + 前缀",算法为:取绝对值 → 以 10 为底对数向下取整 → 再向下归整到 3 的倍数(Floor(exponent/3)*3)→ 数值除以对应 10 的幂。它特殊处理了换算后恰好为1000.0的情况(此时进位,例如1000 k归一为1 M)。
7.3 反向解析ParseSI
ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil):通过init()中动态构建的正则(si.go)拆出数字、前缀与单位,再用反向前缀表(revSIPrefixTable,值为10^指数)还原数值。
八、English 子包:英文复数与词列连接
humanize/english子包提供面向英文文案的辅助函数,原文档(README.markdown)给出了完整示例:
8.1 复数
english.PluralWord(1, "object", "") // object english.PluralWord(42, "object", "") // objects english.PluralWord(2, "bus", "") // buses english.PluralWord(99, "locus", "loci") // loci english.Plural(1, "object", "") // 1 object english.Plural(42, "object", "") // 42 objects english.Plural(2, "bus", "") // 2 buses english.Plural(99, "locus", "loci") // 99 lociPluralWord:只返回单词本身(单数或复数形式),数量为 1 时返回单数原词,否则按规则加s;第三个参数允许提供不规则复数(如locus → loci);Plural:返回"数字 + 空格 + 单词"的完整表达。
8.2 词列(Word series)
english.WordSeries([]string{"foo"}, "and") // foo english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and bazWordSeries用连接词(如and)拼接英文列表,超过两个元素时用逗号分隔并在最后一个元素前插入连接词;OxfordWordSeries额外在连接词前加上牛津逗号(foo, bar, and baz)。
注:在 Tempo 仓库的 vendor 目录中,vendor/github.com/dustin/go-humanize 固化的是核心包文件(
bytes.go、comma.go、ftoa.go、si.go、times.go、ordinals.go、big.go、bigbytes.go、number.go、humanize.go等);english子包按原文档说明位于humanize/english,本文以其文档示例为准。
九、在 Tempo 仓库中的实际使用场景
go-humanize 在 Tempo 中以 vendor 依赖形式存在(go.mod 声明github.com/dustin/go-humanize v1.0.1,vendor/modules.txt 记录其 vendor 条目)。它的典型价值在于把调试输出变得直观:例如在 modules/generator/processor/spanmetrics/spanmetrics_test.go 的测试日志中,通过humanize.Bytes(uint64(batch.Size()))打印 batch 大小,让82854982这样的原始字节数在测试输出里变成一眼可读的83 MB形式。
这种"数字 → 人类可读字符串"的转换在分布式追踪系统的日常运维中尤其有用:块(block)大小、批次字节数、历史时间戳、基数统计等原始数值,经过Bytes/Time/Comma格式化后,日志与指标面板的可读性会显著提升。你可以在自己的监控组件或工具链中,以完全相同的方式引入并使用这套 API。
十、小结与最佳实践
- 字节展示:日志与 UI 中默认推荐
IBytes(IEC 语义,避免 1024/1000 混淆),面向客户或通用场景可选Bytes(SI 语义);需要还原数值时用ParseBytes配合完整后缀表。 - 相对时间:默认
Time的档位模板已经足够自然;如果产品有特殊粒度需求(如精确到天、弱化分钟),用CustomRelTime传入自定义RelTimeMagnitude分档表即可,注意模板中%s(方向标签)与%d(数量)的占位约定。 - 数字格式化:整数分组用
Comma,浮点分组用Commaf,去除尾随零用Ftoa,跨数量级换算用SI/SIWithDigits;超大数使用BigComma/BigBytes。 - 英文文案:拼接日志或错误提示中的数量词时,用
english.Plural/PluralWord,并记得为不规则名词提供第三个参数。 - 引用方式:本文全部实现细节均可直接在仓库的 vendor/github.com/dustin/go-humanize 目录下逐文件核对,涉及的具体文件包括 bytes.go、times.go、comma.go、ftoa.go、si.go 与 ordinals.go。
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考