zap.newproduction() 默认同步写入,因无goroutine、缓冲区或队列,磁盘或网络慢时会阻塞业务;真异步需手动组合encoder、writesyncer(如lock+addsync+lumberjack)和sampler,并实现带channel的writesyncer或调用logger.sync()确保日志不丢失。

直接用 zap.NewProduction() 不能实现异步——它默认是同步写入,磁盘慢或日志量大时会卡住业务逻辑。真要异步,必须手动组合 zapcore.Core,绕过开箱即用的封装。
为什么 zap.NewProduction() 不是异步的
zap.NewProduction() 返回的 logger 底层仍是同步 WriteSyncer,只是用了 JSON 编码和采样,并未引入 goroutine 或缓冲区。它在高并发下仍会因 write() 系统调用阻塞主线程,尤其当日志目标是慢盘或网络 sink(如 Kafka)时。
- 它内部调用的是
os.Stderr或你传入的裸*os.File,没有缓冲、没有队列、没有写超时控制 - 所谓“高性能”来自编码器零分配和结构化字段直写,不是 I/O 并发能力
- 压测中常见现象:QPS 上去后,
pprof显示大量时间卡在syscall.Syscall或runtime.futex
手动构造异步 Core 的三个必要组件
异步不是加个 goroutine 就行,Zap 要求你显式组装 zapcore.Core,且三者缺一不可:
-
Encoder:必须用
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),禁用EncodeTime自定义格式(否则每次调用time.Now().Format()触发 GC) -
WriteSyncer:不能直接传
*os.File,得用zapcore.AddSync包一层,再套zapcore.Lock防多 goroutine 写冲突;若写文件,优先用lumberjack.Logger替代裸os.OpenFile -
Sampler:用
zapcore.NewSampler(core, time.Second, 100, 10)控制高频 error 日志,避免缓冲区打满或 OOM;注意第一个参数是原始 core,不是 logger
示例片段:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
lockedWriter := zapcore.Lock(zapcore.AddSync(file))
encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
core := zapcore.NewCore(encoder, lockedWriter, zapcore.InfoLevel)
samplerCore := zapcore.NewSampler(core, time.Second, 100, 10)
logger := zap.New(samplerCore).With(zap.String("service", "api"))
把日志真正“推”进异步队列
Zap 本身不提供异步 wrapper,得靠 zapcore.NewTee + 自定义 WriteSyncer 实现非阻塞投递:
- 别用
go logger.Info(...)—— 这只是并发打日志,不是异步写入,writer 仍会阻塞 - 正确做法:实现一个带 channel 和 worker goroutine 的
WriteSyncer,例如:type asyncWriter struct { ch chan []byte },在Write(p []byte)里把p拷贝后发到ch,worker 从ch消费并写文件 - 更稳妥:用社区方案如
uber-go/zap官方推荐的lumberjack+zapcore.Lock,它虽非纯异步,但内部有缓冲和 sync 控制,比裸文件稳定得多 - 关键提醒:进程退出前必须调用
logger.Sync(),否则 channel 里未消费的日志会丢失
字段构造阶段就别拖慢主线程
异步只解决 I/O 阻塞,但字段构造本身也可能成为瓶颈——尤其在高频路径上:
-
logger.Info("msg", zap.String("sql", fmt.Sprintf("SELECT * FROM u WHERE id=%d", id)))错在哪?fmt.Sprintf在 logger 判断是否输出前就执行了,即使日志级别是ErrorLevel,它也白跑 - 正确写法:拆成
zap.Int("id", id),让 Zap 在确定要写时才序列化,避免提前分配 - 对复杂值(如 map、struct),别传指针:
zap.Any("req", &r)可能引发竞态;改用zap.Object("req", reqEncoder{r})延迟序列化 - 不确定要不要记的日志,先用
logger.Check(zap.DebugLevel, "msg").Write()显式判断,省掉字段构造开销
真正的异步高性能,是 encoder 零分配 + writer 有锁缓冲 + 字段构造可跳过 + 进程退出前 flush 保障——漏掉任意一环,都可能在凌晨三点丢掉关键错误日志。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










