大家好,我是小蒙。
在 Go 1.21 之前,我们团队的新项目日志库选型基本无脑上 uber-go/zap 或者 rs/zerolog。但自从官方把 log/slog 塞进标准库后,我就一直在关注它的生产可用性。最近几个月,我把公司两个边缘微服务和几个自用小工具的日志模块全部平替成了 slog。
今天不扯虚的,直接上干货,聊聊 slog 在工程化落地中的核心玩法、踩坑点,以及大家最关心的 Benchmark 跑分。
1. 为什么不用第三方库,改用 slog?
直给的结论:性能完全够用,标准库生态统一,无第三方依赖包袱。
slog 采用了 Handler 架构设计,核心概念就四个:
Logger:前端暴露的 API(如slog.Info,slog.Error)。Record:单条日志的底层数据结构。Handler:后端处理器接口(决定写到控制台、文件、还是 Kafka)。Attr:键值对属性。
标准库自带了 TextHandler 和 JSONHandler,基本上 90% 的业务场景开箱即用。
2. 工程化实战一:Context 注入 TraceID
在微服务里打日志,不带 trace_id 约等于白打。slog 提供了 InfoContext 等方法,但标准库默认 Handler 并不会自动把 ctx 里的 TraceID 抽出来。
我们需要封装一个中间件风格的 ContextHandler:
package logx
import (
"context"
"log/slog"
)
type ctxKey string
const TraceIDKey ctxKey = "trace_id"
type ContextHandler struct {
slog.Handler
}
func NewContextHandler(h slog.Handler) *ContextHandler {
return &ContextHandler{Handler: h}
}
func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
if ctx != nil {
if traceID, ok := ctx.Value(TraceIDKey).(string); ok && traceID != "" {
// 将 TraceID 自动追加到日志属性中
r.AddAttrs(slog.String("trace_id", traceID))
}
}
return h.Handler.Handle(ctx, r)
}
初始化全局 Logger:
func InitLogger() {
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: false, // 生产环境高并发建议关闭,有性能损耗
}
baseHandler := slog.NewJSONHandler(os.Stdout, opts)
customHandler := NewContextHandler(baseHandler)
logger := slog.New(customHandler)
slog.SetDefault(logger)
}
3. 工程化实战二:敏感字段脱敏 (ReplaceAttr)
生产环境中,密码、Token、身份证号不能明文输出。slog.HandlerOptions 提供了 ReplaceAttr 钩子,可以在格式化之前修改属性。
func replaceAttr(groups []string, a slog.Attr) slog.Attr {
// 敏感 Key 脱敏
if a.Key == "password" || a.Key == "token" || a.Key == "authorization" {
return slog.String(a.Key, "******")
}
// 统一格式化时间为 UTC RFC3339
if a.Key == slog.TimeKey {
if t, ok := a.Value.Any().(time.Time); ok {
return slog.String(slog.TimeKey, t.UTC().Format(time.RFC3339Nano))
}
}
return a
}
4. 跑分!slog vs zap 性能实测
很多同学最纠结的就是性能,我写了段 Benchmark,对比 zap(生产常用配置)和 slog 在 JSON 模式下的写入吞吐与内存分配。
- 测试环境:Apple M2 Pro / 32GB / Go 1.22.2
- 测试场景:输出一条包含 5 个基础字段(String, Int, Float, Bool, Time)的 JSON 日志,丢弃 I/O(写入
io.Discard)。
goos: darwin
goarch: arm64
pkg: bench/log
BenchmarkZap_JSON-10 7823901 151.2 ns/op 0 B/op 0 allocs/op
BenchmarkSlog_JSON_Typed-10 6541203 182.5 ns/op 0 B/op 0 allocs/op
BenchmarkSlog_JSON_Any-10 4120512 289.4 ns/op 128 B/op 4 allocs/op
剖析总结:
- 强类型传参(Typed)至关重要:使用
slog.String(),slog.Int64()传参时,slog能够做到 0 内存分配 (0 allocs/op),耗时与zap仅差 30ns 左右,这个差距在绝大多数业务场景下完全可以忽略。 - 警惕弱类型松散调用:如果直接写
slog.Info("msg", "key", "val"),会走any装箱,产生额外的堆分配(每次调用 4 次 allocs),吞吐直接下降 35%。
5. 生产落地踩坑警示
-
AddSource性能损耗:AddSource: true会通过runtime.Caller抓取调用栈行号。经实测,开启后单次日志耗时会从180ns暴涨至1200ns+。高 QPS 接口如果狂打日志,CPU 会被抓堆栈吃掉一大截。建议常规 Info 日志关闭,仅在 Error 级别或通过自定义 Handler 单独提取。 -
注意全局实例污染: 使用
slog.SetDefault()会替换全局默认 Logger,但如果你的第三方依赖里用了老旧的log.Println,它也会被重定向到slog的 Default Logger(被解析为没有结构化字段的普通文本)。
结语
从实际落地情况来看,log/slog 的工业完成度非常高。如果你的项目正准备起新服务,或者想摆脱繁重的外部日志框架依赖,完全可以直接上 slog。只要规范好使用强类型方法(如 slog.Int),它的性能和扩展性完全扛得住高并发业务。
大家在用 slog 时有遇到什么奇怪的坑吗?欢迎评论区交流对线。
许可协议:CC BY-NC 4.0
更新于 2 小时前
觉得文章有帮助?点个赞吧!
0 条评论


