生产环境必须用zap.NewProduction(),它默认输出JSON、字段名固定、兼容日志采集器;需caller须显式加zap.AddCaller();文件切割必须用lumberjack;避免全局logger.With()并发污染;关键路径应主动Sync()防panic丢日志。

生产环境必须用 zap.NewProduction(),别碰 zap.NewDevelopment()
开发模式日志带颜色、堆栈、字段展开,体积大、难解析、性能差,filebeat 或 fluentd 读不到结构化字段,直接导致 ELK/Loki 采集失败。生产部署后第一条日志就可能卡在解析环节。
-
zap.NewDevelopment()只该出现在本地调试时,且必须配合zap.AddCaller()才显行号——但它默认不开启,容易误以为“有行号” -
zap.NewProduction()默认输出 JSON,字段名固定(如level、ts、caller被禁用),兼容所有日志采集器 - 如果需要 caller(文件+行号),必须显式加
zap.AddCaller();但注意:它会轻微影响性能,高并发写日志场景建议权衡是否真需要
日志写入文件 + 切割,得靠 lumberjack,Zap 自己不支持
Zap 的核心设计是“只管编码和路由”,不内置滚动、压缩、过期清理逻辑。想把日志落地到 /var/log/app/app.log 并按大小轮转?必须引入 gopkg.in/natefinch/lumberjack.v2。
- 别自己用
os.OpenFile+os.Truncate模拟切割——没原子性,崩溃时易丢日志 -
lumberjack.Logger的MaxSize单位是 MB,不是字节;MaxAge是天数,不是小时,配错会导致日志堆积或过早清理 - 务必用
zapcore.AddSync(&lumberjackLogger)包一层,否则 Zap 不认这个 writer(它只接受实现了zapcore.WriteSyncer接口的对象)
别用全局 logger 塞 trace_id,也别每次手动 logger.With()
并发下全局 logger 调用 With(zap.String("trace_id", tid)) 会串数据;而每个函数都手写 logger.With(...).Info(...) 容易漏、难维护、字段名不统一。
- 正确做法:封装一个
WithContext(ctx context.Context) *zap.Logger方法,在里面提取trace_id并返回新 logger - 避免在中间件或 handler 外层反复调用
With(),应尽早绑定上下文,后续所有子日志自动携带 - 注意
logger.With()返回的是新实例,原 logger 不变;若忘记接收返回值,等于白加字段
defer logger.Sync() 不够,进程崩溃前最后一段日志大概率丢失
Zap 默认用缓冲写入,Sync() 只保证调用时刻之前的数据落盘。kill -9 或 panic 导致进程猝死时,缓冲区里那几十毫秒的日志就没了——尤其是 panic 前的关键 error 日志。
- 验证方法:在 panic 前加一条
logger.Info("about to panic"),然后kill -9进程,检查日志文件末尾是否存在该条 - 缓解方案:对关键路径(如 HTTP handler 入口/出口、DB 查询前后)主动调用
logger.Sync(),但别滥用,会影响吞吐 - 更稳做法:用
zapcore.Lock包装写入器(尤其多 goroutine 写同一文件时),并确保lumberjack的Compress: true关闭(压缩需 rename,非原子)
事情说清了就结束。真正难的不是怎么配 Zap,而是怎么让每条日志在出问题时能被快速定位——字段命名一致性、trace_id 全链路透传、panic 前日志不丢,这些细节比选库重要得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











