登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 数字是底层计数,字符串是面向协议的格式。下面的关系图把字段、数值和单位边界放在一起,阅读代码时先看“协议边界”这一组。

Go time.Duration 从配置字段到纳秒计数和带单位字符串的静态关系说明图
图1:结构说明图,比较 Duration 底层计数、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 都可以成立,关键是名称、文档、校验和转换函数必须一致。若一个服务输出纳秒、另一个服务按毫秒读取,数值通常不会报错,却会让超时扩大或缩小一百万倍,这比显式解析错误更难排查。

Go Duration 字符串解析、数字单位转换和业务配置边界的静态关系说明图
图2:边界说明图,查看字符串解析、明确单位整数和业务层 Duration 的关系。

兼容旧数据时先定规则再写代码

如果已有 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

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>