news 2026/9/18 22:35:38

Grafana Tempo 中的 go-humanize 工具库:让字节、时间与数字输出更人性化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Grafana Tempo 中的 go-humanize 工具库:让字节、时间与数字输出更人性化

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 标准库(fmtstrconvmathsorttimeregexp等),没有任何外部运行时依赖,符合 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 MB79 MiB

2.1 两个入口:BytesIBytes

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),支持数字部分含.,(逗号会被剥离),单位部分不区分大小写,并支持无后缀的简写形式(如kmkimi)。示例:

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/bigBigBytes/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 相对时间算法:RelTimeCustomRelTime

  • RelTime(a, b, albl, blbl):比较两个时间点,时间在前者使用albl标签,在后者使用blbl标签;
  • CustomRelTime(a, b, albl, blbl, magnitudes):允许传入自定义的分档表。

核心算法(times.go):

  1. 计算diff = b.Sub(a),若a晚于b则交换方向并切换标签;
  2. sort.Search在分档表magnitudes中二分查找第一个D > diff的档位;
  3. 按该档位的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 = 24hWeek = 7 * DayMonth = 30 * DayYear = 12 * MonthLongTime = 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 → 0100 → 1001000 → 1,0001000000000 → 1,000,000,000-100000 → -100,000

实现要点(comma.go):

  • 负号单独提取,负数绝对值参与分组;
  • 从低位起每 3 位切一段,不足 3 位时左侧补0再拼接;
  • 特殊处理math.MinInt64:因为MinInt64无法被安全取负,直接返回硬编码字符串-9,223,372,036,854,775,808

5.2CommafCommafWithDigits(float64)

humanize.Commaf(834142.32) // 834,142.32 humanize.CommafWithDigits(834142.32, 1) // 834,142.3

Commaf(comma.go)先按strconv.FormatFloat(v, 'f', -1, 64)转字符串再拆分整数部分分组,小数部分原样保留;CommafWithDigits进一步用stripTrailingDigits限制小数位数。

5.3BigComma(big.Int)

math/big大整数做同样分组的BigComma(comma.go),适合超大数值展示。

六、Ftoa:去掉尾随零的浮点格式化

Go 自带的%f会固定输出 6 位小数,产生2.2400002.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 nM

SI(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 MBSIWithDigits(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 loci
  • PluralWord:只返回单词本身(单数或复数形式),数量为 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 baz

WordSeries用连接词(如and)拼接英文列表,超过两个元素时用逗号分隔并在最后一个元素前插入连接词;OxfordWordSeries额外在连接词前加上牛津逗号(foo, bar, and baz)。

注:在 Tempo 仓库的 vendor 目录中,vendor/github.com/dustin/go-humanize 固化的是核心包文件(bytes.gocomma.goftoa.gosi.gotimes.goordinals.gobig.gobigbytes.gonumber.gohumanize.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 22:34:46

ETL详解:从数据流图到增量抽取与清洗加载的实践指南

简介&#xff1a;一份面向数据仓库初学者与 ETL 开发者的 PPT 课件&#xff0c;围绕 ETL 抽取、转换、加载全流程展开&#xff0c;系统梳理 ETL 定义、实施前提、设计原则、模式比较及常见问题处置&#xff0c;既可用于个人学习&#xff0c;也可作为技术培训或面试准备的参考资…

作者头像 李华
网站建设 2026/9/18 22:34:17

Vue组件样式隔离实战:从scoped原理到CSS Modules与CSS变量方案选型

最近在重构一个 Vue 3 TypeScript 的中后台项目&#xff0c;代码量一上来&#xff0c;最先失控的不是逻辑&#xff0c;而是样式。改一个按钮的颜色&#xff0c;莫名带崩了三个页面的间距&#xff1b;给弹窗加了条边框&#xff0c;结果全局的.card全被顶了一像素&#xff1b;更…

作者头像 李华
网站建设 2026/9/18 22:32:39

使用 Grafana Tanka 在 Kubernetes 上部署 Grafana Tempo

使用 Grafana Tanka 在 Kubernetes 上部署 Grafana Tempo 【免费下载链接】tempo Grafana Tempo is a high volume, minimal dependency distributed tracing backend. 项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo 本文以 Grafana Tempo 官方部署指南为…

作者头像 李华
网站建设 2026/9/18 22:29:18

HMC5883L在STM32F4上的可靠驱动与高精度磁力计校准

1. 为什么HMC5883L在STM32F4上跑不稳&#xff1f;——从“能通信”到“测得准”的真实断层你手里的STM32F4开发板接上了HMC5883L磁力计&#xff0c;I2C扫描能识别0x1E地址&#xff0c;读寄存器也返回了非零值&#xff0c;串口打印出的原始X/Y/Z数据跳变剧烈、方向感混乱&#x…

作者头像 李华