go标准库log包缺乏日志级别、结构化字段和上下文传递能力,导致排查效率低;zerolog适合高吞吐/serverless场景,zap更灵活但启动稍慢;需通过context派生logger并避免全局修改,错误堆栈要用.stack()或.errorstack保留。

为什么直接用 log 包不行
Go 标准库的 log 包只提供基础输出,没有内置级别(debug/info/warn/error)、字段结构化(key-value)、上下文传递能力。你写 log.Printf("user %s failed: %v", uid, err),日志就是纯字符串,后续想按 user_id 过滤或聚合基本靠正则硬扒——线上查问题时效率极低。
真正需要的是:每条日志带明确级别、可序列化为 JSON、能动态注入请求 ID 或 trace ID 等上下文字段。
选 zerolog 还是 zap?看你的启动速度和内存敏感度
zerolog 零分配、无反射、默认输出 JSON,适合高吞吐微服务;zap 性能接近,但初始化稍重,支持更丰富的编码格式(如 console 人类可读格式)和 hook 机制。
如果你的程序对冷启动时间敏感(比如 Serverless 场景),优先用 zerolog;如果需要日志异步刷盘、自定义采样或对接 Loki,zap 的 Core 和 Hook 更灵活。
示例(zerolog 初始化):
import "github.com/rs/zerolog/log"
func init() {
// 输出到 stdout,带时间戳和 level 字段
log.Logger = log.With().Timestamp().Logger()
}
注意:zerolog.SetGlobalLevel(zerolog.InfoLevel) 必须在任何日志调用前设置,否则 debug 日志仍会输出。
如何让日志自动带上 HTTP 请求 ID 和用户 ID
不能每次 log.Info().Str("uid", uid).Msg("login ok") 都手动传——容易漏、难维护。正确做法是用 context.Context + 日志实例绑定。
常见错误:把 *zerolog.Logger 存在全局变量里再改,这会导致并发写 panic。应该用 log.With().Caller().Str("req_id", reqID).Logger() 派生新实例。
- HTTP middleware 中提取
X-Request-ID,注入 context 并派生日志实例 - 业务 handler 中从 context 取出该 logger,直接调用
logger.Info().Msg(...) - 避免在 goroutine 里直接用全局
log,必须显式传入派生 logger
关键点:log.With() 返回新 logger,不污染原实例;log.Ctx(ctx) 是 zerolog 提供的快捷方式,但要求 context 里存的是 *zerolog.Logger 类型值。
JSON 日志里怎么处理 error 类型不丢失堆栈
直接 .Err(err) 只会输出 err.Error() 字符串,丢掉 stack trace。要保留完整错误链,得用 zerolog.ErrorStack 或 zap.Error 的封装。
zerolog 方案:
import "github.com/rs/zerolog/pkgerrors"
// 启用 pkgerrors 支持(需提前 import)
log.Error().Stack().Err(err).Msg("db query failed")
注意:.Stack() 仅在 zerolog.ErrorLevel 及以上生效,且会增加少量开销;生产环境若不需要全量堆栈,可用 fmt.Sprintf("%+v", err) 替代,但会丢失原始 error 类型信息。
另一个坑:error 字段名默认是 error,和 zerolog.ErrorLevel 冲突导致解析失败——务必用 .Err(err).Str("error_message", err.Error()) 显式区分。











