Moby 仓库内嵌 go-humanize 深入指南:在 Go 中人性化格式化字节、时间、序数与 SI 数值
本指南以 Moby 仓库 vendor 树中随包携带的 vendor/github.com/dustin/go-humanize/README.markdown 为蓝本,结合其同一目录下的全部源码实现,系统讲解 go-humanize 在 Go 语言中把枯燥的数字转成人话字符串的完整能力:字节容量(SI/IEC)、相对时间、序数词、千分位逗号、浮点清理、SI 科学前缀以及英文复数/词语串联。读完你能准确理解每个 API 的输出边界与底层算法,并能在自己的 Go 项目中直接、正确地使用这套工具。
库在仓库中的位置与导入方式
在 Moby 仓库中,go-humanize 以 vendored 第三方依赖的形式存放在 vendor/github.com/dustin/go-humanize/ 目录下,包内结构一目了然:
| 文件 | 职责 |
|---|---|
| bytes.go | SI/IEC 字节格式化与解析(Bytes/IBytes/ParseBytes) |
| bigbytes.go | 基于 big.Int 的超大字节格式化与解析 |
| times.go | 相对时间格式化(Time/RelTime/CustomRelTime) |
| ordinals.go | 序数词后缀(Ordinal) |
| comma.go | 千分位逗号(Comma/Commaf/BigComma) |
| ftoa.go | 浮点字符串清理(Ftoa) |
| si.go | SI 前缀格式化与解析(SI/ComputeSI/ParseSI) |
| big.go | big.Int 量级计算辅助函数 |
| number.go | 模板友好的自定义数字格式(FormatFloat) |
包自身的用途在 humanize.go 的包注释里写得很直白:把无聊难看的数字转成对用户友好的字符串(以及再转回来)。导入方式即标准的:
import "github.com/dustin/go-humanize" // 使用时写作 humanize
注:本仓库作为只读的 vendor 快照,仅内嵌了 go-humanize 的顶层包源码;README 中提到的
humanize/english子包属于上游完整发行版的一部分(见下文“英文专用函数”一节)。在 Moby 中依赖的即是本目录这份实现。
字节容量:SI 与 IEC 两种进制一网打尽
大小格式化是 go-humanize 最常用的能力,它让你把 82854982 这样的裸字节数直接变成 83 MB 或 79 MiB。核心入口是两个函数(bytes.go):
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
fmt.Printf("That file is %s.", humanize.IBytes(82854982)) // That file is 79 MiB.
Bytes(SI 十进制,基 1000):依次使用B, kB, MB, GB, TB, PB, EB后缀;IBytes(IEC 二进制,基 1024):依次使用B, KiB, MiB, GiB, TiB, PiB, EiB后缀。
底层由同一个 humanateBytes(s, base, sizes) 函数驱动,算法要点值得了解:
- 字节数小于 10 时直接返回整数形式
"%d B"; - 通过换底公式
log(n)/log(base)计算数量级指数e,据此取出对应后缀; - 数值保留 1 位小数并做四舍五入:
val = floor(s/base^e*10 + 0.5) / 10; - 若换算后
val >= 10,格式化模板退化为"%.0f %s",去掉小数位——这就是82854982经 SI 换算得到 82.9 → 显示为整数83 MB的原因。
对应的单位常量与后缀解析表也定义在同一文件中:bytes.go。IEC 常量用 1 << (iota * 10) 递推得到 Byte=1, KiByte=1024, …;SI 常量按 *1000 递推;bytesSizeTable 把 b/kib/kb/mib/mb/… 等大小写不敏感(解析时统一 ToLower)的单位名映射成常量,还额外兼容了无全名的短形式(如 k、ki、m、mi)。
反向解析 ParseBytes
格式化是单向的,而 ParseBytes 负责把“人话字符串”解析回字节数:
humanize.ParseBytes("42 MB") // => 42000000, nil
humanize.ParseBytes("42 mib") // => 44040192, nil
实现逻辑为:先从左往右扫描数字/小数点/逗号,把数字部分与单位部分切开(数字中的逗号会被剔除),数字用 ParseFloat 解析后乘以查表得到的单位常量;若结果溢出 uint64 或单位无法识别,则分别返回 "too large: ..."、"unhandled size name: ..." 错误。
相对时间:把 time.Time 翻译成“多久以前/以后”
相对时间功能让 time.Time 直接说出“人话”——12 seconds ago 或 3 days from now:
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
入口 Time(then) 等价于 RelTime(then, time.Now(), "ago", "from now"),即过去的时间自动带上 “ago”、未来的时间自动带上 “from now”。更通用的写法允许自定义任意时间点与标签:
// a 早于 b 时用 albl(这里是 "earlier"),a 晚于 b 时用 blbl(这里是 "later")
humanize.RelTime(timeInPast, timeInFuture, "earlier", "later") // => 3 weeks earlier
自动切档的量级表
times.go 中的 defaultMagnitudes 是核心:它按时间差从小到大排布了 16 个档位,从 <1s → now、2s → 1 second %s、1min → %d seconds %s,一路经过分钟、小时、天、周、月、年,直到 LongTime(37 * Year)之后统一显示 a long while %s。每个档位由 RelTimeMagnitude 三元组构成:
D:该档位的起算时长;DivBy:真实差值除以它得到显示的数量(例如档位 D 为 2 分钟、显示%d minutes时 DivBy 取time.Minute);Format:可含%d(数量)与%s(ago/from now标签)两个占位符。
因此输出会随时间的推移自动进位换单位:7 小时会显示 7 hours ago,而不是 420 minutes ago。
自定义量级:CustomRelTime
如果默认档位不符合业务(比如想以“小时”为最小粒度,或为某个具体时长定制措辞),可自行构造升序排列的 []RelTimeMagnitude 交给 CustomRelTime,即可获得完全自主的相对时间文案。这正是 go-humanize 在“格式化”之外给出的扩展点——连特殊情形的 1 second/1 minute/1 hour/1 day/1 month(单数时省略数量词)都由量级表文案负责。
序数词:0th、1st、2nd、3rd
邮件列表里“想要正确标注序数”的需求催生了 Ordinal:
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
对应规则一览(注意 11/12/13 的特例):
| 输入 | 输出 |
|---|---|
| 0 | 0th |
| 1 | 1st |
| 2 | 2nd |
| 3 | 3rd |
| 4 | 4th |
| 11 / 12 / 13 | 11th / 12th / 13th |
| 21 / 22 / 23 | 21st / 22nd / 23rd |
| 193 | 193rd |
实现上先默认 th 后缀,再对个位判断 st/nd/rd,唯一的防呆逻辑是:当 x%100 落在 11、12、13 时必须维持 th(否则 11 会被错误写成 11st)。
千分位逗号:Comma / Commaf / BigComma
“往数字里塞逗号”看似简单,边界却很多:
humanize.Comma(0) // 0
humanize.Comma(100) // 100
humanize.Comma(1000) // 1,000
humanize.Comma(1000000000) // 1,000,000,000
humanize.Comma(-100000) // -100,000
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
comma.go 提供了三个函数覆盖不同数据宽度:
| 函数 | 输入 | 示例 | 特点 |
|---|---|---|---|
Comma(v int64) |
有符号 64 位整数 | 834142 → 834,142 |
特判 math.MinInt64(绝对值无法直接取负),硬编码返回 -9,223,372,036,854,775,808 |
Commaf(v float64) |
浮点数 | 834142.32 → 834,142.32 |
用 FormatFloat(...,'f',-1,64) 最短表示后,整数部分自右向左每三位补逗号,小数部分原样保留 |
BigComma(b *big.Int) |
任意大整数 | — | 基于 big.Int 循环 DivMod 1000 分组,宽度不受 int64 限制 |
配套的 CommafWithDigits(f, decimals) 可截断小数位(注意是截断而非四舍五入),例如 CommafWithDigits(834142.32, 1) → 834,142.3。
Ftoa:去掉多余尾零的浮点字符串
Go 标准库 %f 默认固定 6 位小数,打印 2.0 会得到 2.000000。go-humanize 的 Ftoa 通过“先按 'f', 6 格式化,再从字符串尾部剥掉连续 0 与多余小数点”的办法,输出整洁的十进制表示:
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
若需要限定小数位数,可使用 FtoaWithDigits(num, digits),先截断再剥尾零(例如限制 2 位且去零)。值得注意:这里的去尾零是字符串级裁剪,不引入 math.Round 的浮点误差,这也是它与传统 strconv 用法互补的价值所在。
SI 科学前缀:q → Q 的完整标度
对于电子、物理量纲等场景,SI 前缀格式化让数字自动匹配恰当量级:
humanize.SI(0.00000000223, "M") // 2.23 nM
humanize.SI(1000000, "B") // 1 MB
前置函数 ComputeSI(input) 返回归一化后的数值与对应前缀(si.go):它取输入绝对值的常用对数并按 3 的倍数向下取整指数,把数值缩放进 [1, 1000) 区间,同时处理了一个特例——若归一化后恰为 1000.0,则进位一格(输出 1 M 而非 1000 k)。SI(input, unit) 再叠加上调用方提供的单位字符串,并通过 Ftoa 保证不留尾零。
解析侧 ParseSI 能把带前缀的字符串还原:
humanize.ParseSI("2.2345 pF") // => (2.2345e-12, "F", nil)
内置前缀表覆盖 q(quecto,1e-30) 到 Q(quetta,1e30) 共 21 档(含正负幂),代码逐行注释了每个缩写对应的词源(micro 用 µ)。数值部分支持可选的 +/- 号与千分位小数,且数字与字母间允许空格。
超大字节与 big.Int 支持
当数值超出 uint64 时,bigbytes.go 以 math/big 给出了无损版本:BigBytes/BigIBytes 分别输出 SI 与 IEC 格式,容量一路延伸到 QB/QiB(1e30 / 1024^10);ParseBigBytes 把 "42 MB" 这类字符串解析成 *big.Int,过程中借助 big.Rat 完成任意精度小数乘法再取整,避免中间溢出。对于记账、存储系统配额等对精度敏感的场景,应优先选用这一组 API,而不是把值硬塞进 uint64。
英文专用函数(上游 english 子包)
按 README 的说明,以下函数位于 go-humanize 的 humanize/english 子包,面向英文文案的本地化写作(在本仓库的 vendor 快照中未包含该子包,使用前需引入上游完整包):
英文复数(PluralWord 只返回单词,Plural 附带数量前缀):
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
第三个参数允许显式传入不规则复数形式(如 locus → loci),未提供时走常规加 s/es 的规则。
词语串联(WordSeries 用逗号与连词组织列表,OxfordWordSeries 额外输出牛津逗号):
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
实践建议小结
围绕 README 与源码,最后给出几条可落地的选择标准,方便在真实项目中快速决策:
- 容量文案选进制:操作系统/内存、文件系统等二进制场景选
IBytes(MiB 基 1024);带宽、磁盘标称等十进制营销规格选Bytes(MB 基 1000),二者单位写法不同,切勿混用(详见 bytes.go)。 - 相对时间要自定义粒度:默认量级表粒度到秒;若日志、监控只需分钟级,请基于
CustomRelTime与RelTimeMagnitude自建档位(times.go)。 - 大数不截断:涉及超过
int64/uint64的字节量或金额,一律走BigBytes/BigComma系列。 - 反向解析同样重要:格式化与解析(
ParseBytes/ParseBigBytes/ParseSI)成对设计,适合做配置回读、校验或数据入库前的归一化。
对于在容器生态里需要把磁盘用量、镜像大小、日志时间等底层指标呈现给终端用户的 Go 服务,这套 vendored 实现提供了“格式化—展示—再解析”的完整闭环,值得直接复用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00