标准库 log 够用,适用于 CLI 工具、小型服务等轻量场景;但需结构化日志、高 QPS 或上下文注入时,应换 slog 或 zap,并注意 slog 参数偶数约束、zap caller 配置及 handler 切换须用显式 logger 实例。

log 包够用吗?先看场景再决定要不要换
标准库 log 不是“过时”,而是“有边界”。它适合 CLI 工具、小型服务、原型验证——只要不追求结构化、不需多级日志控制、不接入可观测平台,log 就足够轻量且稳定。
但一旦涉及以下任一情况,就该考虑替代方案:
- 需要把
user_id、request_id这类字段自动注入每条日志(log无法绑定上下文) - 日志要被 ELK 或 Loki 解析,必须输出 JSON 格式(
log只能拼字符串) - QPS 超过 5k,发现日志写入开始拖慢主流程(
log是同步阻塞 I/O)
别为了“看起来专业”而引入 zap;也别在微服务里硬扛 log 做 trace 关联——边界清晰,选型才不翻车。
slog.Info 传参奇数个会静默失败
slog.Info 的 key-value 写法看着简单:slog.Info("msg", "user_id", 123, "status", "ok"),但参数个数必须是偶数(key + value 成对)。如果漏掉一个值,比如写成 slog.Info("msg", "user_id", 123, "status"),编译通过,运行时不报错,但 "status" 后面所有字段全丢弃,连 warning 都没有。
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
这不是 bug,是设计取舍。解决办法很直接:
- 开发期装
sloglint:go install github.com/rogpeppe/go-internal/cmd/sloglint@latest,然后在 CI 或本地 pre-commit 里跑sloglint ./... - 关键路径改用
slog.String/slog.Int显式构造:slog.Info("msg", slog.Int("user_id", 123), slog.String("status", "ok")),类型安全,IDE 也能补全
zap.NewProduction() 默认不带 caller,别直接上生产
zap.NewProduction() 看名字像“开箱即用”,但它默认关闭 caller(即文件名+行号),这对排查问题极其不友好。你看到的只有 {"level":"error","msg":"failed to parse json"},却不知道哪一行代码抛的错。
正确做法是显式启用并控制开销:
- 开发/测试环境:用
zap.NewDevelopment(),自带 caller、彩色输出、堆栈(仅限 error) - 生产环境:不要直接用
NewProduction(),而是组合配置:cfg := zap.NewProductionConfig() cfg.DisableCaller = false cfg.EncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder logger, _ := cfg.Build()
- 注意:开启
caller会有轻微性能损耗(每次日志都调runtime.Caller),但比起定位问题的成本,这点开销值得
日志 Handler 切换不能只改初始化代码
很多人以为用 slog 就能“换 Handler 不动业务代码”,但实际有个隐藏前提:所有日志调用必须通过同一个全局 *slog.Logger 实例,而不是直接调 slog.Info 这类包级函数。
错误写法(Handler 切换无效):
slog.SetDefault(slog.New(jsonHandler))
slog.Info("hello") // ✅ 走 jsonHandler
// 后来想切 OTel
slog.SetDefault(slog.New(otelslog.NewHandler(nil)))
slog.Info("world") // ❌ 仍走旧的 jsonHandler —— 因为 SetDefault 只影响后续新 goroutine
正确写法(确保统一入口):
var logger *slog.Logger
func init() {
logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
}
func handleRequest() {
logger.Info("request received", "path", r.URL.Path)
}这样换 Handler 只需改 init() 里的构造逻辑,所有 logger.Info 自动生效。
真正容易被忽略的是:goroutine 生命周期和 slog.SetDefault 的作用域不一致,尤其在 HTTP handler 中起新 goroutine 时,SetDefault 可能失效。所以别依赖它,用显式 logger 实例最稳。

















