直接用slog.NewTextHandler或JSONHandler不够用,因字段顺序、时间格式、键名(如time→@timestamp)、error展开等均不可控;需自定义Handler实现Handle方法,组合内置Handler做装饰,并注意线程安全与嵌套处理。

为什么直接用 slog.NewTextHandler 或 slog.NewJSONHandler 不够用
因为它们只支持预设的文本或 JSON 格式,字段顺序、时间格式、键名(比如把 time 改成 @timestamp)、甚至是否展开 error 结构体都不可控。一旦需要对接 ELK、Loki 或内部日志系统,往往得改字段名、加 trace_id、统一时间 RFC3339 微秒级精度,这时必须自己实现 slog.Handler 接口。
怎么写一个最小可用的自定义 slog.Handler
核心是实现 Handle(r slog.Record) error 方法,再嵌入 slog.HandlerOptions 控制属性(如是否加源码位置)。不要从零写,而是组合已有的 TextHandler 或 JSONHandler 做装饰:
- 用
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})作为底层,再重写Handle去修改r的字段 - 关键点:不能直接改
r.Attrs()返回的迭代器——它只读;要用slog.Group或slog.Attr重建记录 - 时间字段必须用
r.Time,别调time.Now(),否则丢失原始打点时间 - 若要加固定字段(如
service=api),在Handle开头用slog.With包一层更安全,避免污染原始Record
type MyHandler struct {
inner slog.Handler
}
func (h MyHandler) Handle(r slog.Record) error {
// 添加 service 字段,并把 time 改为 @timestamp
r.AddAttrs(slog.String("service", "api"))
r.Attrs(func(a slog.Attr) bool {
if a.Key == "time" {
// 替换 key,保持 value 不变
r.AddAttrs(slog.String("@timestamp", a.Value.String()))
return false
}
return true
})
return h.inner.Handle(r)
}
如何让自定义 Handler 支持 WithGroup 和属性嵌套
原生 TextHandler 对 slog.Group 的处理是扁平化键名(http.status),但有些后端要求嵌套 JSON 对象。这时不能依赖默认行为,得在 Handle 中手动展开 Group:
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
- 遍历
r.Attrs(),遇到a.Value.Kind() == slog.KindGroup时,递归提取子字段 - 用
slog.Group构造新Attr时,key 是空字符串,否则会多一层无意义外层 - 注意:如果底层用的是
JSONHandler,它原生支持嵌套;但TextHandler不支持,强行嵌套会导致输出乱码,所以优先选JSONHandler作底 - 测试时用
slog.With("http", slog.Group("status", slog.Int("code", 200)))验证是否生成"http": {"status": {"code": 200}}
容易被忽略的性能与线程安全细节
slog.Handler 实例会被多个 goroutine 并发调用,但 Record 本身不是线程安全的——你不能缓存它的字段到全局变量里,也不能在 Handle 中复用某个 bytes.Buffer 而不重置:
- 每次
Handle必须新建临时 buffer 或使用sync.Pool获取,用完Reset() - 避免在
Handle里做阻塞操作(如 HTTP 请求、磁盘写),这会拖慢整个应用日志链路 -
slog.Record.Level是slog.Level类型,打印前需转成字符串用r.Level.String(),别直接fmt.Sprintf("%v", r.Level),输出是DEBUG(4)这种 - 如果用了
HandlerOptions.ReplaceAttr,它会在所有字段上运行,包括你手动加的字段,所以逻辑里要检查key == ""(表示是 group 的 root)或跳过已处理字段
真正麻烦的不是写 Handler,而是验证它在高并发下不丢字段、不 panic、不污染其他 log 记录——建议用 testing.T.Parallel() 跑 1000 次带 Group 和 Error 的日志,再 grep 输出校验结构。

















