微服务中不能直接使用标准库log,因其缺乏结构化、上下文支持和性能优化。slog需注意key-value参数偶数个,推荐显式构造;zap默认关闭caller,需手动开启;HTTP中间件应统一注入request_id并透传上下文。

log 包在微服务里直接用会出问题,结构化、上下文、性能三方面都扛不住。该换就换,别硬撑。
微服务里为什么不能只用标准库 log
标准库 log 是字符串拼接式日志,没有字段概念,也没法自动注入 request_id 或 trace_id。微服务一跑起来,所有日志混在一起,根本没法按请求串起来。更麻烦的是它同步写文件,QPS 高了会拖慢整个 HTTP 处理流程——你看到的“接口变慢”,可能只是日志卡住了。
- 日志没结构 → Loki/ELK 解析不了
"user_id=123 status=ok"这种字符串 - 无法绑定上下文 → 每次
log.Printf都得手动传一遍request_id,漏一次就断链 - 无日志级别控制 → 不能单独关闭
DEBUG日志,生产环境全量输出 - 不支持 caller 信息 → 错误日志里看不到哪行代码出的问题
slog 的 key-value 参数必须偶数个
slog.Info("msg", "user_id", 123, "status", "ok") 看着简单,但少写一个值,比如 slog.Info("msg", "user_id", 123, "status"),运行时不报错也不警告,后面所有字段直接丢弃。“status”这个 key 就没了,连 msg 都可能被当成 value 覆盖。
- 开发期装
sloglint:go install github.com/rogpeppe/go-internal/cmd/sloglint@latest,CI 里跑一遍 - 关键路径改用显式构造:
slog.Info("msg", slog.Int("user_id", 123), slog.String("status", "ok")),类型安全,IDE 补全也跟得上 - 别依赖字符串顺序传参,尤其在封装中间件时,容易漏字段
zap.NewProduction() 默认不带 caller
名字叫 NewProduction,但默认关掉了 caller 字段(即 file:line),你只会看到 {"level":"error","msg":"failed"},不知道错在哪。这不是配置遗漏,是设计默认关——得自己开。
- 开发/测试用
zap.NewDevelopment(),自带 caller、彩色、堆栈(error 级别) - 生产环境别直接
zap.NewProduction(),要显式配置:cfg := zap.NewProductionConfig(); cfg.DisableCaller = false; cfg.EncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder - 开启
caller有轻微开销(每次调runtime.Caller),但比花两小时翻代码定位强得多
HTTP 中间件里注入 request_id 并透传到日志
不是在每个 handler 里手动生成 request_id,而是在入口中间件统一做:生成 ID、塞进 context、写入响应头、再挂到 logger 上。否则下游服务或 goroutine 里拿不到上下文,日志就断了。
立即学习“go语言免费学习笔记(深入)”;
- 用
github.com/oklog/ulid生成时间有序 ID,比纯uuid更适合排查排序 - 中间件里用
context.WithValue(r.Context(), "request_id", reqID),别用context.WithValue(context.Background(), ...)—— 会丢掉原有 context - logger 封装函数必须接收
ctx context.Context,从中取request_id并用.With()挂载,而不是每次打点都手动传 - 如果调下游 HTTP 服务,记得把
X-Request-ID从 header 透传过去,否则链路在第一个跳就断了
context 传递,或者少配一个 caller,排查成本就翻倍。


















