生产环境高频写入、核心链路、压测场景必须用zap.Logger,因其零分配、无反射、强类型字段保障性能与结构化;日常开发、调试、原型阶段可用zap.SugaredLogger,支持松散传参但有反射和字符串拼接开销。

zap.Logger 和 zap.SugaredLogger 选哪个?
直接看场景:高频写入、核心服务链路、压测环境,用 zap.Logger;日常业务逻辑、快速原型、调试阶段,用 zap.SugaredLogger。
zap.Logger 不做类型转换、不走反射、字段必须显式构造(比如 zap.String("user_id", id)),性能高但写法略重;zap.SugaredLogger 支持 sugar.Infow("user login", "user_id", 123, "duration", time.Second) 这种松散传参,底层会做类型推断和转换,开销略大但开发效率高。
- 别在 HTTP handler 里混用两者——同一模块保持一致,否则日志字段结构不统一,后续查 ELK 时字段缺失或类型错乱
-
zap.SugaredLogger的Infof/Warnf看似方便,但会触发 fmt.Sprintf,实际比Infow多一次字符串拼接,高并发下易成瓶颈 - 如果已用
zap.SugaredLogger,又想提升性能,可只对关键路径(如 DB 查询、RPC 调用)临时转回zap.Logger,用sugar.Desugar()获取底层实例
如何让日志同时输出到控制台和文件?
不能简单调用两次 zap.New,那样会创建两个独立的 Core,无法保证写入顺序和原子性。正确做法是复用同一个 Core,但配置多个 WriteSyncer。
关键点在于:用 zapcore.NewMultiWriteSyncer 合并输出目标,再传给 zapcore.NewCore:
立即学习“go语言免费学习笔记(深入)”;
console := zapcore.Lock(os.Stdout)
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
multi := zapcore.NewMultiWriteSyncer(console, zapcore.Lock(file))
encoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
core := zapcore.NewCore(encoder, multi, zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.InfoLevel
}))
logger := zap.New(core).Named("main")
- 务必对
os.File加zapcore.Lock,否则多 goroutine 写文件会乱序或截断 - 控制台用
ConsoleEncoder,文件建议用JSONEncoder,避免解析困难;若需颜色,仅限开发环境开启EncodeLevel颜色函数 - 别漏掉
defer logger.Sync(),否则进程退出前缓冲区日志可能丢失
按时间轮转日志文件,为什么 lumberjack 不够用?
lumberjack 只支持按大小或时间单维度切割,且无法按日志级别分离文件——比如你希望 error.log 单独存、info.log 按天滚动,它做不到。
真实生产需求是「双维度路由」:先按级别分流,再各自按时间归档。得自己实现 zapcore.WriteSyncer:
- 封装一个
LevelWriter,内部维护 map[string]zapcore.WriteSyncer,key 是 level 名("error"、"info") - 实现
Write(p []byte) (n int, err error)时,先解析 JSON 日志里的"level"字段,再路由到对应 writer - 每个子 writer 接
lumberjack.Logger,设置MaxAge: 7、LocalTime: true - 注意:JSON 解析要轻量,推荐用
bytes.IndexByte(p, '"')扫描,而非json.Unmarshal,避免分配
字段注入 context.TraceID 时容易丢数据
很多人在 middleware 里用 logger.With(zap.String("trace_id", traceID)),然后传给 handler——这看似合理,但有个致命问题:With 返回新 logger,如果 handler 里没继续传递或覆盖,下一层调用仍用原始 logger,trace_id 就丢了。
真正可靠的方式是把 logger 塞进 context.Context:
ctx = context.WithValue(r.Context(), loggerKey{}, logger.With(zap.String("trace_id", traceID)))
// handler 中取:
logger := ctx.Value(loggerKey{}).(zap.Logger)
logger.Info("request handled")
- 别用
context.WithValue(ctx, "logger", ...)——字符串 key 易冲突,定义私有类型作 key 更安全 - 如果用了
zap.SugaredLogger,记得用sugar.With(...).Desugar()转成zap.Logger再塞 context,否则下游无法统一调用logger.Info - HTTP 请求结束时,一定要调用
logger.Sync(),否则 trace 相关日志可能滞留在 buffer 里
Zap 封装最难的部分不在语法,而在写入路径的可控性:WriteSyncer 是否线程安全、Encoder 是否复用 buffer、Core 的 level check 是否提前拦截——这些细节一旦出错,轻则日志重复/丢失,重则 GC 飙升拖垮服务。


















