Go 的 time.Time 默认 JSON 序列化为 RFC 3339 格式,国内接口需“2024-05-20 14:23:18”格式,必须通过自定义类型(如 type DateTime time.Time)并用指针接收器实现 MarshalJSON/UnmarshalJSON;json:",string" tag 无效,因仅触发 MarshalText 输出仍为 RFC 3339;UnmarshalJSON 需处理 null、空串、多格式解析(优先完整时间→仅日期→RFC 3339),layout 必须为“2006-01-02 15:04:05”;推荐类型别名而非嵌入结构体以零开销继承 time.Time 方法。

Go 的 time.Time 默认 JSON 序列化用 RFC 3339(如 "2024-05-20T14:23:18.123Z"),国内接口普遍要 "2024-05-20 14:23:18" 这类格式——不能靠改 tag 或全局配置解决,必须自定义类型并实现 MarshalJSON/UnmarshalJSON。
为什么不能用 json:",string" tag?
加 string tag 只会让标准库调用 Time.MarshalText(),输出仍是 RFC 3339(只是包了引号),比如 "2024-05-20T14:23:18Z",不是你要的空格分隔格式。它不接受 layout 参数,也绕不开纳秒/时区冒号等细节。
常见错误现象:
- 前端解析报错:收到带
T和Z的字符串,new Date()失败或时区偏移 - MySQL 插入失败:字段是
DATETIME,但值含T或纳秒部分 - 日志难读:时间字段太长,且不同环境输出不一致(因纳秒精度不同)
必须用类型别名 + 指针接收器实现 UnmarshalJSON
定义新类型(如 DateTime)为 time.Time 别名,再实现两个方法。关键点:
立即学习“go语言免费学习笔记(深入)”;
-
UnmarshalJSON必须用指针接收器(*DateTime),否则无法修改原值 - 必须处理
null、空字符串、零值,否则解析 JSONnull会 panic - layout 字符串只能是 Go 基准时间
"2006-01-02 15:04:05",写成"%Y-%m-%d"会静默失败
示例:
type DateTime time.Time
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)
if err != nil {
return err
}
*dt = DateTime(t)
return nil
}
多个格式共存时,UnmarshalJSON 要依次尝试 time.Parse
如果 API 入参可能传 "2024-05-20"、"2024-05-20 14:23:18"、甚至 "2024-05-20T14:23:18",不能只写一个 time.Parse。得按优先级逐个试,返回第一个成功结果:
- 先试完整格式(含时分秒)
- 再试仅日期
- 最后 fallback 到 RFC 3339(兼容旧数据)
注意:顺序很重要。如果把 "2006-01-02" 放前面,"2024-05-20 14:23:18" 会被截断解析成当天零点。
嵌入结构体 vs 类型别名:选轻量、零开销的后者
别用 struct{ time.Time } 嵌入方式——它多一层内存布局,且 time.Time 方法需显式调用 t.Time.Before(),不如直接用别名方便。类型别名(type MyTime time.Time)无额外开销,所有 time.Time 方法可直接调用(如 t.Add(time.Hour)),语义也更清晰。
容易被忽略的点:
- 别在 handler 里手动
fmt.Sprintf时间再拼 map —— 后续加字段易漏,反序列化完全失效 - 别用
string字段存时间再解析 —— 丢掉时区、比较、加减能力,校验逻辑散落在各处 - Go 1.22+ 的
json.MarshalOptions目前不支持时间格式定制,别白费劲找配置项


















