Go time.Duration 序列化成 JSON 怎么避免单位误解
来源:17golang原创
时间:2026-11-10 23:15:34 394浏览 收藏
Go 的 time.Duration 本质上是一个以纳秒为单位的 int64。因此,直接把它交给传统的 encoding/json 时,1500 * time.Millisecond 会变成 JSON 数字 1500000000,调用方很容易误读成 1500 毫秒、1500 秒,甚至不知道单位。更稳妥的做法是先选定协议:跨语言配置优先传带单位的字符串;必须传数字时,就把单位写进字段名和接口文档。
如果目标是可读配置,使用"1.5s"这样的字符串并在 Go 端调用time.ParseDuration;如果目标是数字协议,则使用timeout_ms或timeout_ns,不要让裸数字代表一个没有名字的 Duration。
先看清 time.Duration 的默认含义
官方 time 包把 Duration 定义为两个时间点之间的耗时,底层是纳秒计数,最大可表示范围约为 290 年。它的 String 方法则会输出 1h30m0.5s、250ms 这类带单位文本;ParseDuration 能读取 ns、us、µs、ms、s、m、h。
这两个表示不能混为一谈:JSON 数字是底层计数,字符串是面向协议的格式。下面的关系图把字段、数值和单位边界放在一起,阅读代码时先看“协议边界”这一组。

可读配置使用带单位字符串
对于超时、退避间隔、缓存 TTL 等配置,我更愿意把字符串作为公开契约。它能让配置审查和跨语言对接直接看出单位,解析失败也能在入口处返回错误,而不是把一个错误的裸数字带进业务。
package main
import (
"encoding/json"
"fmt"
"time"
)
// JSONDuration 把 Duration 固定为带单位的 JSON 字符串。
type JSONDuration struct {
time.Duration
}
// MarshalJSON 保留 1.5s、250ms 这样的可读单位。
func (d JSONDuration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Duration.String())
}
// UnmarshalJSON 在配置入口解析单位并拒绝非法文本。
func (d *JSONDuration) UnmarshalJSON(data []byte) error {
var raw string
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("duration must be a string: %w", err)
}
parsed, err := time.ParseDuration(raw)
if err != nil {
return fmt.Errorf("invalid duration %q: %w", raw, err)
}
d.Duration = parsed
return nil
}
type Config struct {
// RetryAfter 的 JSON 值示例为 "1.5s",不再依赖隐含单位。
RetryAfter JSONDuration `json:"retry_after"`
}
这样编码后的字段是 "retry_after":"1.5s",解码时也只接受有明确单位的字符串。若协议规定零值合法,0s 会自然保留;若零值表示“未配置”,应在配置层增加指针或显式开关,而不是让调用方猜测。
数字协议要把单位写进名字
有些旧接口只能传数字,或者下游已经固定为整数毫秒。这时可以继续使用整数,但不要直接暴露 time.Duration 的 JSON 默认结果。字段名应把单位写出来,转换只发生在边界处:
type NumericConfig struct {
// 字段名明确约定:这里的数字永远是毫秒。
TimeoutMS int64 `json:"timeout_ms"`
}
func (c NumericConfig) Duration() time.Duration {
// 只在进入 Go 业务层时转换,避免内部到处重复乘单位。
return time.Duration(c.TimeoutMS) * time.Millisecond
}
func fromDuration(d time.Duration) NumericConfig {
// 取整策略要写进协议;这里向零截断为毫秒。
return NumericConfig{TimeoutMS: d.Milliseconds()}
}
timeout_ms: 1500 和 timeout_ns: 1500000000 都可以成立,关键是名称、文档、校验和转换函数必须一致。若一个服务输出纳秒、另一个服务按毫秒读取,数值通常不会报错,却会让超时扩大或缩小一百万倍,这比显式解析错误更难排查。

兼容旧数据时先定规则再写代码
如果已有 JSON 中保存的是裸数字,第一步不是直接改成字符串,而是查清历史数字的真实单位。传统 encoding/json 对 time.Duration 的默认数字表示是纳秒;但某个业务 DTO 可能早已手工定义成毫秒。迁移时建议按下面的顺序处理:
- 给旧字段建立样本,确认生产数据的单位和取值范围。
- 新增版本化字段,例如同时接受旧的
timeout和新的timeout_text,输出只保留一种规范格式。 - 对负数、超大值、空字符串和小数分别设定规则;不要让溢出或隐式取整静默通过。
- 在跨语言文档中写出 JSON 示例,并注明
time.ParseDuration支持的单位集合。
如果使用的是新的 encoding/json/v2,也不要假设旧的默认行为会自动延续。官方文档说明,Duration 需要显式表示方式;纳秒数字需要使用对应选项。无论采用哪一版 JSON 包,本文的核心原则都不变:协议的单位必须显式可见。
最后用一张表锁定协议选择
| 场景 | 推荐 JSON | 重点边界 |
|---|---|---|
| 人读配置、跨语言接口 | "1.5s" | 入口用 ParseDuration,非法单位直接报错 |
| 兼容固定整数接口 | 1500 配合 timeout_ms | 只在边界转换,明确取整和范围 |
| 旧字段迁移 | 兼容旧读、规范新写 | 先确认旧数据单位,再设计版本策略 |
把 time.Duration 看成“带单位的业务量”,而不是普通整数,JSON 序列化就不会再靠猜。写字符串协议时让值自描述;写数字协议时让字段名自描述,这两条足以挡住大多数单位误解。
常见疑问
为什么不能直接把 Duration 的数字当成毫秒?
因为 Duration 的底层单位是纳秒。只有在字段或转换函数明确约定为毫秒时,数字才有毫秒语义。
字符串一定比数字更好吗?
不一定。字符串更适合可读配置和跨语言协议;高吞吐的固定整数接口可以使用毫秒或微秒,但必须把单位写进字段名、文档和边界转换。
官方资料:https://pkg.go.dev/time#Duration、https://pkg.go.dev/encoding/json
-
237 收藏
-
125 收藏
-
322 收藏
-
349 收藏
-
485 收藏
-
272 收藏
-
337 收藏
-
471 收藏
-
161 收藏
-
497 收藏
-
190 收藏
-
119 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习