log.Println() 不是异步日志,因其同步格式化、全局锁、无缓冲导致高并发下性能瓶颈;最简异步方案是带缓冲 channel + 单后台 goroutine 消费;生产推荐 zap.NewAsyncCore,已内置无锁缓冲、双触发刷盘等健壮机制。

为什么不能直接用 log.Println() 做异步日志
它根本不是异步的——格式化、写入、锁竞争全在主线程。每条日志都触发一次 fmt.Sprintf(),还共用全局 sync.Mutex。QPS 上千时,几十个 goroutine 会卡在锁上排队;磁盘一慢,整个 handler 就挂住;没缓冲、没背压,突发日志洪峰直接导致 channel 阻塞或 OOM。
chan *LogEntry + 单后台 goroutine 是最简可行方案
核心就三件事:定义带缓冲 channel、启动唯一消费协程、业务侧只发不等。不依赖第三方库,适合快速落地。
-
logChan := make(chan *LogEntry, 1024)——缓冲区必须设上限,1024 是经验值,再大内存风险陡增 - 后台协程必须唯一:多个 goroutine 并发写同一文件,不加锁就会日志错乱(比如时间戳和消息对不上)
- 别在循环里每次调用
file.WriteString(),改用bufio.NewWriter(file)包一层,攒够 16KB 或 50ms 再Flush() - 关键错误日志要绕过异步通道:
zap.WrapCore+zapcore.NewTeeCore(syncCore, asyncCore),让ERROR直写并fsync
生产环境优先用 zap.NewAsyncCore(),别手撸
自己实现容易漏掉滚动切割、崩溃恢复、OOM 保护这些细节。zapcore.NewAsyncCore 已经把无锁环形缓冲、定时/满载双触发 flush、错误监控都封装好了,实测比手写快 3~5 倍。
-
zapcore.NewAsyncCore默认缓冲 1024 条,超时 1 秒强制刷盘,参数可调但没必要轻易动 - 它默认不保证顺序——如果日志顺序对调试至关重要(比如 trace 链路),得额外加序号或用同步 core 处理关键路径
- 注意
Close()调用时机:必须在程序退出前显式调用,否则缓冲中日志会丢失
异步日志的关闭与背压控制必须显式处理
很多手写方案只管发不管收,close(logChan) 后没等 worker 完成最后一波 flush,就直接退出,导致日志截断。更隐蔽的问题是:当磁盘满或文件句柄耗尽时,worker 协程可能卡死,而业务侧还在不停往 chan 里塞日志,最终 channel 满、default 分支丢日志,却没人感知。
立即学习“go语言免费学习笔记(深入)”;
- worker 循环里必须有
select+default或带超时的send,防止业务 goroutine 长期阻塞 - 提供
Close()方法:先close(ch),再sync.WaitGroup.Wait()等待 worker 刷完剩余日志 - 建议加一个
DropCount指标,记录被丢弃日志数,用 Prometheus 暴露,避免“看起来正常实则静默丢日志”
zap 默认做了,但你要知道它在哪做了、怎么关、怎么查。


















