zap.NewProduction() 创建的日志实例默认不输出到控制台,仅写入 os.Stderr 且在非 TTY 环境禁用彩色与换行,需显式调用 Info() 等方法并满足 InfoLevel 及以上才可见。

zap.NewProduction() 创建的日志实例默认不输出到控制台
很多人以为 zap.NewProduction() 会像开发环境那样把日志打到终端,结果一跑发现什么都没看到——其实它默认只往 os.Stderr 写,且在非 TTY 环境(比如容器、systemd)下会自动禁用彩色和换行优化;更关键的是,如果你没显式调用 logger.Info() 等方法,或者日志等级被过滤(Production 默认是 InfoLevel 及以上),就真的一条都看不到。
-
zap.NewProduction()返回的 logger 是「生产就绪」配置:结构化 JSON、时间戳纳秒级、无 caller 信息、无堆栈(除非显式开启) - 它内部使用
zapcore.Lock(os.Stderr),所以多 goroutine 写日志是安全的,但你得确保 stderr 没被重定向或关闭 - 如果想临时看日志,加一句
logger.Info("startup", zap.String("status", "ok")),别只初始化不调用 - 常见错误:直接
fmt.Println(logger)—— 这只会输出指针地址,不是日志内容
为什么不能直接用 zap.NewProduction() 做开发调试
因为它的输出格式全是 JSON 字段,没有可读性,也没有文件名 / 行号,连 logger.Debug() 都不会输出(Production 默认最低是 Info)。
- 开发时该用
zap.NewDevelopment():带颜色、带 caller、支持Debug级别、输出格式易读 - 二者核心差异在
EncoderConfig和LevelEnabler:前者用consoleEncoder,后者用jsonEncoder;别试图给NewProduction()换 encoder,它封装死了 - 如果硬要在生产环境加 caller,得自己构造
zap.Config,而不是依赖NewProduction() - 注意:
NewProduction()不做任何缓冲,写日志就是同步 syscall,高并发写大日志可能卡 goroutine
如何安全地替换默认 logger(避免 panic)
zap 全局 logger 是通过 zap.ReplaceGlobals() 设置的,但它不是线程安全的——如果多个包同时调用 zap.L() 之前还没初始化好,就会 panic“no global logger”。
- 务必在
main()最开头(甚至 init 函数里)就完成全局 logger 初始化:zap.ReplaceGlobals(zap.NewProduction()) - 不要在包级变量里直接调用
zap.L(),比如var log = zap.L()—— 此时全局 logger 还没设,会 panic - 推荐方式:把 logger 当参数传入函数,或通过接口注入,而非强依赖全局实例
- 如果必须用全局 logger,确保所有
init()函数都不依赖它;否则先用nil占位,等 main 中初始化完再赋值
zap.NewProduction() 的性能陷阱:字段太多会拖慢
它本身很快,但如果你每次 log 都塞一堆未序列化的结构体(比如直接传 map[string]interface{} 或自定义 struct),zap 会在运行时反射取字段,开销陡增。
立即学习 “go 语言免费学习笔记(深入)”;
- 永远用
zap.String()、zap.Int()、zap.Object()等明确类型的方法,别用zap.Any()处理复杂值 -
zap.Any()在 production encoder 下会触发完整 JSON marshal,比zap.Object()+ 自定义LogObjectMarshaler慢 3–5 倍 - 避免在 hot path(如 HTTP handler 内部)频繁创建新字段:复用
zap.String("key", val)比拼接字符串再传进去更轻量 - 日志量极大时,考虑用
zap.CheckWriteSyncer包一层异步 writer,但要注意丢失最后几条日志的风险
真正难的不是怎么调用 zap.NewProduction(),而是理解它背后那套 encoder / core / level / syncer 的协作链条——改错一个环节,整条日志就静默了。