log.Printf 高并发下成瓶颈因默认使用全局互斥锁,所有调用串行化;zap 无锁、零分配、支持异步,生产用 NewProduction(),需显式 Sync() 防丢失,Error/Warn 禁用采样。
log.Printf 在高并发下会成为性能瓶颈
因为默认的 log.Logger 内部使用了全局互斥锁(mu),每次调用 log.Printf 或 log.Println 都会阻塞其他 goroutine。在 QPS 上千的服务中,日志写入可能占到 CPU 时间的 15% 以上,尤其当输出到文件或网络时延迟放大更明显。
sprintf)在主 goroutine 中同步执行,加剧阻塞os.Stderr,系统调用开销不可忽略zap 替换标准库日志的最小可行配置zap 是目前 Go 生态中性能最稳定的结构化日志库,其 Logger 实例是无锁、零分配(对常见场景)且支持异步写入的。关键不是“要不要用”,而是“怎么避免踩坑”:
zap.NewProduction(),它自动禁用堆栈捕获、启用 JSON 编码、设置合理采样率zap.NewDevelopment(),但切勿在压测或线上开启 WithCaller(true)
logger.With() 创建新实例——它会复制字段,产生小对象逃逸logger := zap.NewProduction()
defer logger.Sync() // 必须显式调用,否则异步日志可能丢失
// ✅ 推荐:复用带字段的 logger 实例
requestLogger := logger.With(zap.String("path", r.URL.Path))
requestLogger.Info("request received", zap.Int("status", 200))
// ❌ 避免:每次请求都 With 字段
logger.With(zap.String("path", r.URL.Path)).Info("request received")

zap 的 Sync() 不仅刷新缓冲区,还负责回收后台 goroutine 资源。若服务退出前未调用,可能导致进程 hang 住或日志丢失;而重复调用 Sync() 则会 panic(sync: unlock of unlocked mutex)。
main 函数退出前或 http.Server.Shutdown 回调中调用一次 logger.Sync()
Sync()
zap.L()(全局 logger),需先通过 zap.ReplaceGlobals() 替换为自定义实例,再统一管理生命周期默认的 zapcore.NewSampler 每秒最多记录 100 条相同模板日志,超出则丢弃。但这个阈值在微服务链路追踪中容易误杀关键错误(比如某次 DB 连接失败被采样掉)。
Warn 和 Error 级别应禁用采样:zapcore.NewSampler(core, time.Second, 0, 0)
Info 级别可按模块分级:API 层设为每秒 50 条,缓存层设为每秒 5 条panic、fatal 日志启用采样——它们本就不该高频出现真正难处理的,是那些既高频又必须留痕的日志,比如用户登录成功。这时候得靠业务侧加开关(如 if loginCount%100 == 0)做粗粒度降频,而不是依赖日志库的采样器。