Zap 本身是同步日志库,所谓“异步”需手动用 channel + goroutine 封装实现:分离日志生成与写入,避免业务阻塞;需设缓冲 channel(如1024)、启动消费 goroutine、提前序列化字段,并处理满载超时。

zap 不是异步日志库,它本身不带异步写入能力
Zap 的 NewProduction() 或 NewDevelopment() 返回的 logger 是同步的——所有日志调用都会阻塞直到写入完成。所谓“异步”,必须靠你自己封装:用 channel + goroutine 消费日志条目,再交由 zap 写入。硬把 logger.Info() 当成异步用,结果只是把阻塞从 handler 移到了 goroutine,没解决根本问题。
用 channel + goroutine 实现真正异步日志落地
核心是分离「日志生成」和「日志写入」两个阶段,避免业务逻辑等 I/O。常见错误是 channel 缓冲区太小或没设超时,导致高负载下 goroutine 堆积或主流程卡死。
- 定义带缓冲的 channel:
logCh := make(chan *zapcore.Entry, 1024),1024 是经验阈值,低于 256 容易丢日志,高于 4096 易 OOM - 启动独立 goroutine 消费:
go func() { for entry := range logCh { _ = core.Write(entry) } }(),注意core必须是线程安全的(如已用zapcore.Lock()包裹) - 在业务中不直接调用
logger.Info(),而是构造zapcore.Entry后发往 channel;字段需提前序列化,避免在消费 goroutine 中触发反射 - channel 满时用
select { case logCh ,绝不阻塞
WriteSyncer 必须加锁且禁用颜色/缩进
裸写文件或 stdout 在多 goroutine 下会 panic,Zap 要求所有 WriteSyncer 是并发安全的。同时,JSON 输出若含换行或颜色字符,会被采集器(如 Promtail)当多行日志切碎。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 文件写入必须包一层
zapcore.Lock(zapcore.AddSync(file)),不能直接传*os.File - 编码器必须用
zapcore.NewJSONEncoder,且禁用EncodeLevel的颜色:EncodeLevel: zapcore.LowercaseLevelEncoder - 时间格式必须用
zapcore.ISO8601TimeEncoder,别用time.Now().Format()——后者每次调用都分配内存,压测时 GC 频繁 - 开发环境可保留
ConsoleEncoder,但生产环境严禁启用
defer logger.Sync() 不等于异步安全
logger.Sync() 只保证缓冲区 flush 到底层 writer,但它本身是同步阻塞调用。如果异步 channel 里还剩未消费日志,Sync() 会等它们全写完才返回——这违背了异步初衷。
立即学习“go语言免费学习笔记(深入)”;
- 进程退出前,应先关闭 channel:
close(logCh),再等消费 goroutine 自然退出 - 不要依赖
defer logger.Sync()来保日志不丢;真正的保障是 channel 缓冲 + 内存队列 + 优雅 shutdown 信号 - 若用
lumberjack轮转日志,Sync()还要额外处理文件切换,容易卡住,建议改用zapcore.Lock(zapcore.NewMultiWriteSyncer(...))组合多个 sink
实际落地最易被忽略的点:异步不是加个 goroutine 就完事,关键在 channel 容量、entry 构造时机、writer 并发安全、shutdown 顺序 四者必须咬合。少一个,要么丢日志,要么卡请求,要么吃内存。

















