Go slog 为什么 JSON 时间格式不符合预期
来源:17golang原创
时间:2026-10-22 04:42:13 491浏览 收藏
Go slog 的 JSON 时间格式不符合预期,通常是把“合法时间字符串”理解成了“固定时区、固定小数位的展示模板”,或修改了业务属性却没有修改内置日志时间。应先确认字段来源,再用 JSONHandler 的 ReplaceAttr 明确时区、精度和 JSON 类型;不要给日志再追加一个同名 time 字段。
slog 官方文档:https://pkg.go.dev/log/slog
时间布局官方文档:https://pkg.go.dev/time#Time.Format
先确认改的是哪一个时间字段
设想一个任务服务的采集器要求顶层 time 是 UTC、保留三位毫秒,但开发者只格式化了 scheduled_at。两者当然可能看起来不同:前者来自记录的 Record.Time,后者是业务传入的属性。修改其中一个,不会自动为另一个建立相同策略。
先把 Record.Time、time、scheduled_at、JSONHandler、ReplaceAttr 和采集器分开看。JSONHandler 负责序列化;ReplaceAttr 是字段改写入口;采集器则有自己的类型与解析约定。这个问题涉及字段契约,不一定涉及 Go 版本升级。

默认格式为什么不等于固定毫秒
时间的时区、小数精度和文本布局是三个不同选择。UTC 与带偏移的表示可以指向同一时刻,但文字不相同;纳秒精度也不意味着字符串永远有九位小数。Go 的 RFC3339Nano 布局会去掉尾部零,不能拿它当固定九位模板。
若下游必须接收三位毫秒,可以明确使用 2006-01-02T15:04:05.000Z07:00,并在 Format 之前调用 UTC。这里的数字是 Go 时间布局的参考时刻,不是任意日期,也不是其他语言的 YYYY-MM-DD 模板。三个零要求固定三位,小数部分的纳秒信息不会全部保留。
| 目标 | 需要明确什么 |
|---|---|
| 统一 UTC | 格式化前转换到 UTC,而不是只删掉偏移文本 |
| 固定三位毫秒 | 使用 .000 布局,接受更细精度的信息损失 |
| 保留可变纳秒精度 | 允许尾零缩短,不按字符串长度判断时间有效性 |
| 数值毫秒时间戳 | 字段改成数字,并同步修改采集器类型规则 |
用 ReplaceAttr 统一内置日志时间
下面是原创的完整示例。为了让阅读时能判断格式,示例手工创建固定时间的 Record,再调用 handler;应用的常规日志可用相同配置创建 Logger。代码不依赖第三方库,不写文件,不启动服务。
package main
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"time"
)
func emitRecord(w io.Writer) error {
h := slog.NewJSONHandler(w, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// 只处理顶层保留键和时间类型,不把分组中的业务 time 一并改写。
if len(groups) == 0 && a.Key == slog.TimeKey && a.Value.Kind() == slog.KindTime {
text := a.Value.Time().UTC().Format("2006-01-02T15:04:05.000Z07:00")
return slog.String(a.Key, text)
}
return a
},
})
// 固定时刻只用于示例,常规 Logger 会为日志创建记录时间。
at := time.Date(2026, 8, 17, 9, 12, 30, 456789000, time.UTC)
r := slog.NewRecord(at, slog.LevelInfo, "任务已登记", 0)
// 业务时间用不同键保留原有语义,不与内置 time 重名。
r.AddAttrs(slog.Time("scheduled_at", at.Add(2*time.Hour)))
return h.Handle(context.Background(), r)
}
func main() {
// 直接调用 Handle 时由调用方接收并处理 writer 返回的错误。
if err := emitRecord(os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, "写日志失败:", err)
os.Exit(1)
}
}
按示例布局,顶层 time 的预期文本为 2026-08-17T09:12:30.456Z。JSON 中它仍是字符串,不是毫秒整数;scheduled_at 则没有被该回调转换。这些是根据示例可推导的结果,不是本机实测日志。
回调返回原属性表示不改写,返回零值 Attr 则会丢弃属性。不要为了换格式直接返回零值,否则时间字段消失是代码指定的行为,而非 JSONHandler 丢失时间。示例在局部创建中间文本,不保留或修改 groups 切片。

业务时间与分组字段单独约定
ReplaceAttr 会遇到非分组属性及分组内部的属性。仅判断 a.Key == "time" 范围过宽,可能改到业务分组中的同名字段;因此示例同时限制顶层和 KindTime。即便如此,顶层业务属性若也取名 time 且是时间类型,仍无法只靠这些条件与内置字段区分,所以保留键应当留给日志系统。
如果业务确实要求 scheduled_at 也遵守同一协议,就为这个键明确增加规则,并说明它是“任务计划时间”,不是日志产生时间。若业务时间被包在结构体或 map 属性内部,不要假设 ReplaceAttr 会递归改写任意对象的所有成员;那部分序列化与普通 slog 属性改写不是同一个层级。
同样,已经传入字符串的时间并非 KindTime,示例会保持它不变。这是一种有意的保守策略:与其猜测任意字符串是否为日期,不如在字段产生处决定类型。若某字段来自外部输入,应先完成其解析和错误处理,再交给日志接口。
迁移采集规则时保留哪些边界
把输出从默认表示换成固定毫秒,是日志协议的变化,而不是单纯美化。上线前应在自己的项目里确认采集器接受新布局、时区没有被重复转换、精度降低不会影响查询需求,以及历史日志与新日志是否需要双格式兼容。本文只提供写法,没有执行项目测试或线上采集验证。
若要数字毫秒,回调可以使用 slog.Int64 与 UnixMilli,但应作为另一套明确契约,而不是同一个字段今天输出字符串、明天输出数字。数字纪元时间戳不携带展示时区,界面显示时再决定时区;不能靠把键名改成 timestamp 就解决类型不一致。
改 TextHandler 会同步改变 JSONHandler 吗? 不会修改另一个已经创建的 handler。应把策略放在实际写出 JSON 的那份配置中,并确认业务使用的是对应 Logger。
为什么自定义 Record 没有内置 time? 零值记录时间会省略内置时间;想换格式与想删除时间是两个需求,先检查 Record.Time 是否为零。
为什么小数位缩短不代表时间精度计算错了? 文本布局可以去掉尾零,同一时刻能够有不同长度的合法表示。按下游协议解析字段,不要只比较字符串长度。
最稳妥的修改范围是先统一内置日志时间,再逐项约定业务时间。把来源、时区、精度和 JSON 类型写清楚,采集端才不必从看似相似的字符串里猜测语义。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
232 收藏
-
163 收藏
-
310 收藏
-
101 收藏
-
488 收藏
-
255 收藏
-
197 收藏
-
461 收藏
-
120 收藏
-
209 收藏
-
291 收藏
-
122 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习