数据库时间必须存UTC,这是跨时区应用的硬性前提;解析时间需用ParseInLocation并加载正确IANA时区名(如"Asia/Shanghai"),GORM写入前须设loc=UTC并转UTC,API应返回带Z的UTC字符串。

数据库里存的时间必须是 UTC,否则跨时区应用早晚出事。这不是最佳实践,而是硬性前提——只要用户分布在不同时区,又想让“2024-05-20 10:00”在东京和上海都对应同一物理时刻,就只能靠 UTC 锚定时间轴。
time.LoadLocation("Asia/Shanghai") 为什么总返回 nil
传错时区名是最常见原因。time.LoadLocation 只认 IANA 官方时区名,不接受 "CST"、"GMT+8"、"China/Beijing" 这类写法。
- ✅ 正确:
shanghai, err := time.LoadLocation("Asia/Shanghai") - ❌ 错误:
time.LoadLocation("CST")→ 返回nil,后续调用t.In(shanghai)会 panic - ⚠️ 注意:
"UTC"和"Local"是内置名,但time.Local依赖宿主机配置,Docker 容器里常是 UTC,不可用于业务逻辑 - 调试技巧:打印
time.Now().Zone()看当前环境识别出的时区名和偏移
ParseInLocation 解析字符串时的三个关键点
用 time.Parse 直接解析带时区的字符串,90% 情况下结果不对——它默认走 time.Local,不是你想要的“按字符串里写的 +08:00 去解释”。
- 必须用
time.ParseInLocation,且 layout 要跟字符串时区部分严格匹配:"2006-01-02T15:04:05-07:00"对应"+08:00","2006-01-02T15:04:05Z"对应Z - 第二个参数(时区)别硬写
time.Local,应该加载目标时区:shanghai, _ := time.LoadLocation("Asia/Shanghai"),再传进去 - 如果字符串末尾是
+08:00但你想存成 UTC,解析完立刻调t.UTC(),而不是等入库前再转——避免中间环节误用本地时区
GORM 写入 MySQL 前必须做的两件事
GORM 默认不处理时区,time.Time 字段直接塞进 DATETIME 或 TIMESTAMP 字段,行为取决于 MySQL 配置和连接参数,极易不一致。
立即学习“go语言免费学习笔记(深入)”;
- 连接字符串加
&loc=UTC:user:pass@tcp(127.0.0.1:3306)/db?parseTime=true&loc=UTC - 写入前显式转 UTC:
record.CreatedAt = time.Now().UTC(),别依赖 GORM 自动转换 - ⚠️ 注意:
TIMESTAMP类型在 MySQL 中会自动转为服务器时区存储,DATETIME则原样存;统一用TIMESTAMP+loc=UTC更可靠 - 验证方法:查数据库原始值,确认是
2024-05-20 02:30:00这类无偏移格式,而非2024-05-20 10:30:00
API 返回时间字段不能靠前端猜时区
后端返回 "created_at": "2024-05-20T10:30:00Z" 是安全的;返回 "2024-05-20T10:30:00"(无 Z 无偏移)等于埋雷。
- Go 的
time.Time.MarshalJSON默认输出 ISO8601 带时区格式,只要没手动.Format()成无时区字符串,就 OK - 如果业务要求显示为北京时间,用
t.In(shanghai).Format(time.RFC3339),确保末尾是+08:00,不是Z - 禁止在返回前做
t.UTC().Format(...)再拼 JSON 字符串——这会让前端误以为那是用户本地时间,尤其当用户在东京时,10:30Z显示成19:30就乱套了 - 最省心做法:所有 API 时间字段统一返回 UTC 字符串(含
Z),前端按需转换,后端不承担展示责任
真正难的不是转换函数怎么写,而是所有环节——从 HTTP 请求解析、到内存计算、再到数据库写入和 API 输出——都得守住“UTC 是唯一真相”这条线。任何一个环节偷偷用了 time.Local 或字符串中转,就会在某个凌晨三点的夏令时切换时刻,让你的定时任务集体偏移一小时。


















