Go 的 time.Time 默认 JSON 序列化使用 RFC 3339 格式,不满足国内接口常见的“2024-05-20 14:23:18”等需求;根本原因是 MarshalJSON 不支持自定义 layout 和字段级控制,最可靠方案是定义新类型并实现 MarshalJSON/UnmarshalJSON 方法。

Go 的 time.Time 默认 JSON 序列化不满足业务需求
Go 标准库对 time.Time 的 JSON 序列化默认输出 RFC 3339 格式(如 "2024-05-20T14:23:18.123Z"),但多数国内接口要求 "2024-05-20 14:23:18" 或 "2024-05-20"。直接用结构体字段类型为 time.Time 会导致前后端格式错位,且无法区分“仅日期”和“带毫秒”的场景。
根本原因在于:标准 json.Marshal 调用的是 Time.MarshalJSON(),它不接受自定义 layout,也不支持按字段粒度控制格式。
- 不要试图在 HTTP handler 里手动
fmt.Sprintf时间字段再拼 JSON —— 破坏结构体统一序列化逻辑,后续加字段易漏 - 别用
string类型存时间再解析 —— 失去类型安全、时区处理能力弱、校验逻辑分散 - 避免全局替换
json.Marshal—— 影响第三方库、测试 mock 困难、不可控
给结构体字段加 json tag 并实现 MarshalJSON/UnmarshalJSON
最可控的方式是为需要定制格式的时间字段定义新类型,并实现 JSON 编解码方法。这样既保留 time.Time 的全部能力,又可按字段指定格式。
例如统一用「年月日 时分秒」格式:
立即学习“go语言免费学习笔记(深入)”;
type DateTime time.Time
func (dt DateTime) MarshalJSON() ([]byte, error) {
t := time.Time(dt)
if t.IsZero() {
return []byte(`null`), nil
}
return []byte(`"` + t.Format("2006-01-02 15:04:05") + `"`), nil
}
func (dt *DateTime) UnmarshalJSON(data []byte) error {
s := strings.Trim(string(data), `"`)
if s == "" || s == "null" {
*dt = DateTime(time.Time{})
return nil
}
t, err := time.Parse("2006-01-02 15:04:05", s)
*dt = DateTime(t)
return err
}
- 必须用指针接收
UnmarshalJSON—— 否则无法修改原值 - 注意处理空字符串和
"null",否则解析失败 panic - layout 字符串必须是 Go 的固定基准时间
"2006-01-02 15:04:05",写成"%Y-%m-%d %H:%M:%S"会静默失败 - 若需支持多种格式(如同时兼容
"2024-05-20"和"2024-05-20 14:23:18"),得在UnmarshalJSON里依次尝试time.Parse
用 json.MarshalOption(Go 1.22+)统一配置时间格式?
Go 1.22 引入了 json.MarshalOptions,但它**不支持覆盖 time.Time 的序列化行为**。目前该机制只对实现了 Marshaler 接口的类型生效,而标准 time.Time 没有开放 hook。所以即使你写了:
opts := json.MarshalOptions{UseNumber: true}
// 这不会改变 time.Time 的输出格式
结论很明确:不能靠新 API “一键统一”,仍需类型封装或中间层转换。
- 别被文档里 “options” 名字误导 —— 它不提供 format 控制项
- 社区已有封装库(如
github.com/lunixbochs/struc)做类似事,但引入依赖会增加维护成本 - 如果项目已用 ORM(如 GORM),注意它的
CreatedAt/UpdatedAt字段默认仍是标准格式,需单独配置serializer或重写字段类型
API 层统一拦截时间字段(适合存量项目快速修复)
当结构体太多、无法逐个改字段类型时,可在 HTTP handler 入口做轻量转换:先用标准 json.Unmarshal 解析到 map[string]any,递归查找 key 名含 "time"、"at"、"date" 的字段,按规则转成目标格式后再塞回结构体。但这只是权宜之计。
- 性能有损耗(多一次反序列化 + 遍历),QPS 高时明显
- 字段名判断容易误伤(比如
username里有"time") - 无法处理嵌套结构体中的时间字段,除非写完整 JSONPath 遍历逻辑
- 真正可靠的长期方案,还是回到字段级类型封装 —— 看似多写几行,实则省掉后续所有排查时间格式 bug 的工时
MarshalJSON,而是得想清楚:这个时间字段,到底是“精确到秒的创建时间”,还是“用户选的生日(仅日期)”,或是“带毫秒的日志时间”。格式只是表象,语义才是关键。










