不能直接用zap.NewProduction()开箱即用,因其默认启用同模板日志采样(每秒最多100条,其余静默丢弃),高频请求日志易丢失;且写文件需显式加zapcore.Lock()保障线程安全,否则多goroutine下必panic。

为什么不能直接用 zap.NewProduction() 开箱即用
它默认开启采样(sampling),高频日志如每个 HTTP 请求都打点,zap.Error 或 zap.Info 可能被静默丢弃——你看到的“没日志”不是配置漏了,是日志被 Zap 主动砍掉了。生产环境必须显式禁用或重配采样策略,否则排查问题时会发现关键请求日志完全缺失。
-
zap.NewProductionConfig()返回的配置里Sampling非 nil,需手动设为nil - 若保留采样,至少调大阈值:
cfg.Sampling = &zapcore.SamplingConfig{Initial: 100, Thereafter: 100} - 开发阶段建议用
zap.NewDevelopmentConfig()并关掉采样,避免误判日志丢失
写文件必须加 zapcore.Lock(),否则多 goroutine 下 panic
裸传 *os.File 给 zapcore.AddSync() 是线程不安全的。Gin 每个请求都在独立 goroutine 执行,多个协程同时写同一文件句柄会触发 write on closed file 或 invalid argument 错误。这不是偶发,是必然崩溃。
- 正确写法:
zapcore.Lock(zapcore.AddSync(file)) - 别用
io.MultiWriter(os.Stdout, file)直接塞给 Zap——Zap 的WriteSyncer接口要求同步语义,MultiWriter 不满足 - 轮转日志推荐
lumberjack.Logger,但它的Write方法本身非线程安全,仍需外层套zapcore.Lock
Gin 中间件里别反复调用 logger.With()
每次 logger.With(zap.String("req_id", id)) 都会新建一个 *zap.Logger 实例,内部复制 encoder、core 和字段 map。QPS 上千时,这部分内存分配和 GC 压力远超日志内容本身。
- 预置通用字段:初始化全局 logger 时就加上
zap.String("service", "api")、zap.String("env", "prod") - 请求级字段用
c.Set("logger", logger.With(...))存到 context,中间件内直接取,不重复构造 - 绝对不要在
c.Next()前后各调一次With()——合并成一次,带齐所有字段再记录
时间格式化必须用 zapcore.ISO8601TimeEncoder,禁用自定义 time.Now().Format()
Zap 的零分配设计依赖编译期确定的编码路径。一旦你在 EncoderConfig.EncodeTime 里写 func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("2006-01-02T15:04:05Z07:00")) },每次打日志都会触发字符串分配和 GC,压测中 CPU 时间 30% 花在 runtime.mallocgc 上。
- 生产环境固定用:
EncodeTime: zapcore.ISO8601TimeEncoder - 开发环境可选
zapcore.ISO8601TimeEncoder或zapcore.EpochTimeEncoder,但绝不用time.Now().Format - 如果必须定制格式(如去掉毫秒),用
zapcore.TimeEncoderOfLayout("2006-01-02T15:04:05Z07:00"),它是预编译的
最易被忽略的是采样和锁——前者让日志“看起来没写”,后者让服务“跑着跑着就崩”。这两个点不处理,其他配置再漂亮,上线后都会出问题。











