Go标准库中time.Time的MarshalJSON方法硬编码为RFC3339格式,Gin默认使用该逻辑;需通过实现json.Marshaler接口(如自定义LocalTime类型)、handler中手动Format或封装CustomJSON等方式定制格式。

为什么 time.Time 默认序列化成 RFC3339 而不是你想要的格式
Gin 默认使用 Go 标准库的 json.Marshal,而 time.Time 的 MarshalJSON 方法硬编码返回 RFC3339 格式(如 "2024-05-20T14:23:15+08:00"),根本不管你的 time.Format("2006-01-02") 想法。这不是 Gin 的 bug,是 Go 的设计决定。
想改格式,必须绕过默认序列化逻辑。常见错误是试图在 struct tag 里写 json:"create_time,2006-01-02" —— 这完全无效,tag 不解析时间格式。
- 真正起作用的是实现
json.Marshaler接口 - 或者用
gin.JSON前手动转成字符串字段 - 全局改会影响所有接口,慎用
json.Marshal替换
给结构体字段加 MarshalJSON 方法最稳妥
为自定义时间类型(比如 type LocalTime time.Time)实现 MarshalJSON(),就能精准控制每个字段的输出格式。比改全局 JSON 库安全,也比每次手动 map[string]interface{} 干净。
示例:
type LocalTime time.Time
func (t LocalTime) MarshalJSON() ([]byte, error) {
st := time.Time(t).Format("2006-01-02")
return []byte(`"` + st + `"`), nil
}
type User struct {
Name string `json:"name"`
CreatedAt LocalTime `json:"created_at"`
}
- 注意:必须返回带双引号的字符串字节,否则 JSON 解析失败
- 如果要支持空值(nil time),需额外判断
time.Time(t).IsZero() - 别用
time.Local直接 Format —— 容器环境时区可能不是你本地,建议显式用time.In(loc)
用 gin.Context.JSON 前临时转换字段
适合快速修复单个接口、或字段少且不复用的场景。不改结构体定义,直接在 handler 里构造 map。
示例:
user := getUser()
c.JSON(200, gin.H{
"name": user.Name,
"created_at": user.CreatedAt.Format("2006-01-02"),
})
- 简单直接,但字段一多就容易漏、难维护
- 如果
CreatedAt是指针(*time.Time),必须先判空,否则 panic - 不能用于嵌套结构体自动展开,比如
user.Profile.BirthDate得逐层处理
全局替换 json.Marshal 风险高,除非你真需要
有人会想到用 json.Marshal = customMarshal 替换标准函数。Gin 内部确实调用它,但这等于动了整个应用的 JSON 底层,第三方库(如 gorm、redis 序列化)也可能意外受影响。
- 如果你只希望 API 响应走自定义格式,应该只拦截
c.JSON或中间件中重写响应体 - 更安全的做法是封装一个
CustomJSON方法,替代原生c.JSON - 时间格式统一管理建议抽成常量,比如
const DateLayout = "2006-01-02",避免散落各处的 magic string
时间格式这事看着小,但一旦混用 time.Local、UTC、固定时区,加上跨服务传输,很容易在某个凌晨三点爆出来——尤其是前端按本地时区解析后发现日期错了一天。


















