go标准库encoding/json完全忽略time_format等自定义struct tag,因其未实现该解析逻辑;gin的c.json()直接调用该库,故无法通过tag控制时间格式,必须自定义类型并实现json.marshaler/unmarshaler接口。

Go 的 encoding/json 包不支持通过 struct tag(如 json:"created_at,time_format:\"2006-01-02\"")控制时间格式,Gin 默认也沿用它;想让 API 响应里的 time.Time 字段输出 "2024-05-20 14:23:15" 这类格式,必须自定义类型并实现 json.Marshaler 和 json.Unmarshaler 接口。
为什么 Gin 无法直接用 time_format tag 控制格式
Gin 的 c.JSON() 底层调用的是标准库 encoding/json,而该包**完全忽略**任何形如 time_format 的 struct tag。写 json:"updated_at,time_format:\"2006-01-02 15:04:05\"" 不会产生任何效果,字段仍按 RFC 3339 输出(带纳秒、时区偏移、冒号),前端解析可能失败或入库报错。
- 这不是 Gin 的限制,是 Go 标准库的设计决定
- 第三方 JSON 库(如
github.com/goccy/go-json)才支持扩展 tag,但 Gin 默认不集成 - 试图在 controller 里手动
.Format()再塞进 map,会丢失类型安全,且反序列化失效
正确做法:定义 LocalTime 类型并实现 MarshalJSON/UnmarshalJSON
推荐使用嵌入 time.Time 的别名类型(如 type LocalTime time.Time),而非结构体封装——更轻量、零额外内存开销、方法继承清晰。
-
MarshalJSON必须返回带双引号的 JSON 字符串字节切片,不能只返回裸字符串 -
UnmarshalJSON接收者必须是指针(*LocalTime),否则无法修改原值 - 解析时要处理
"null"字符串,否则遇到 null 会 panic - 布局字符串必须严格用 Go 的基准时间
"2006-01-02 15:04:05",写成"yyyy-MM-dd"会直接崩溃
示例关键代码:
type LocalTime time.Time
const timeFormat = "2006-01-02 15:04:05"
func (t LocalTime) MarshalJSON() ([]byte, error) {
if time.Time(t).IsZero() {
return []byte("null"), nil
}
return json.Marshal(time.Time(t).Format(timeFormat))
}
func (t *LocalTime) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
*t = LocalTime(time.Time{})
return nil
}
s := strings.Trim(string(data), `"`)
tt, err := time.Parse(timeFormat, s)
if err != nil {
return err
}
*t = LocalTime(tt)
return nil
}
嵌入结构体时注意字段声明和时区一致性
在 Gin handler 返回的结构体中,字段需声明为 LocalTime 类型,并保持 JSON tag 小写+下划线风格(与多数 REST API 约定一致):
- 字段名必须导出(首字母大写),否则
json包无法访问 - 建议统一用
time.Local或time.UTC解析,避免本地时区导致跨服务器时间偏移(例如部署在 UTC+8 和 UTC+0 的机器上结果不同) - 若业务允许空日期,改用
*LocalTime指针类型,null对应nil,比零值更语义明确 - 数据库交互场景下,还需额外实现
driver.Valuer和sql.Scanner接口(如 MySQLDATETIME字段)
结构体示例:
type UserResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
CreatedAt LocalTime `json:"created_at"` // 自动触发 LocalTime.MarshalJSON
UpdatedAt *LocalTime `json:"updated_at"` // 支持 null
}
容易被忽略的细节:UnmarshalJSON 中的引号处理和零值语义
JSON 字符串在 data []byte 中自带双引号(如 []byte(`"2024-05-20 14:23:15"`)),所以 time.Parse 时不能直接传 string(data),否则会因开头结尾的引号匹配失败而报错 parsing time ""2024-05-20 14:23:15"" as "2006-01-02 15:04:05": cannot parse """ as "2006"。
- 必须先
strings.Trim(string(data), `"`)去掉外层引号 - 零值(
time.Time{})默认是0001-01-01 00:00:00 +0000 UTC,业务上往往代表“无效时间”,应显式返回错误或转为null,而不是静默接受 - 如果 API 要求所有时间字段必填,
UnmarshalJSON遇到空字符串或非法格式应返回具体错误,方便前端定位问题,而不是吞掉
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











