log.Printf在高并发下拖慢程序是因为其内置互斥锁导致goroutine排队,且fmt.Sprintf等格式化操作同步执行、消耗大量CPU。应避免在hot path直接调用,改用zap等结构化日志,并注意字段构造开销。
log.Printf 在高并发下会拖慢程序因为标准库 log.Logger 默认使用互斥锁(mu sync.Mutex)保护输出,所有 goroutine 必须排队写日志。QPS 上千时,锁争用明显,log.Printf 耗时可能从微秒级涨到毫秒级,甚至成为瓶颈。
更隐蔽的问题是:日志格式化本身(如 fmt.Sprintf)在调用线程中同步执行,高频打点时 CPU 时间大量消耗在字符串拼接上,而非真正 I/O。
log.Printf
log.Printf("%s %d %v", s, n, v) 这类需运行时反射的格式化——它比 log.Printf("%s %d %+v", s, n, v) 更慢log.SetFlags(0) 可省掉时间戳/文件名等开销,但代价是丢失上下文信息zap 替换标准库日志的实操要点zap 是目前 Go 生态中性能最主流的选择,核心优势是结构化 + 零分配(zero-allocation)日志构造。但不是简单替换 import 就能见效,关键在初始化和字段写法。
zap.NewProduction() 启动时默认开启缓冲和异步写入,但若日志量极大,仍建议显式配置 zap.AddCaller() 和 zap.IncreaseLevel(zap.WarnLevel) 控制采样logger.Info("msg", zap.String("key", val), zap.Int("count", n)),而非 logger.Info(fmt.Sprintf("msg: %s, count: %d", val, n)) —— 后者绕过了结构化,也失去了延迟格式化能力defer logger.Info("exit", zap.Duration("took", time.Since(start))) 在每请求都触发,应结合采样(如仅 slow request 记录)不是所有日志都要落地,盲目降级(如全切到 Warn)会导致问题难排查。真正有效的是基于请求特征或耗时做动态采样。
zap.SugaredLogger 的 With 方法预置公共字段(如 reqID, userID),避免每次调用重复传参func logIfSlow(logger *zap.Logger, start time.Time, threshold time.Duration, msg string, fields ...zap.Field) {
elapsed := time.Since(start)
if elapsed > threshold {
logger.Warn(msg, append(fields, zap.Duration("elapsed", elapsed))...)
}
}logger.Info("request", ...) 全量打点;改用 logger.Debug("request", ...) 并通过环境变量控制是否启用(zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.DebugLevel && os.Getenv("DEBUG_LOG") == "1" }))不建议。除非你已压测确认 zap 的 Core(尤其是 WriteSyncer)仍是瓶颈,且愿意承担队列溢出、panic 丢失日志、时序

zap 自带的 zapcore.Lock + bufio.Writer 组合已覆盖大多数场景;更高阶需求(如按模块分流、日志分级落盘)应通过 zapcore.NewTee 或自定义 Core 实现,而非裸写 channel + goroutine。
zapcore.NewCore + zapcore.NewSamplerCore 做采样前置,再套一层 zapcore.Lock,比自己建 buffer channel 更稳zap.NewDevelopment() 在生产环境使用——它默认禁用采样、强制同步写、还加了颜色和 caller,性能比 Production 低一个数量级os.O_APPEND | os.O_CREATE | os.O_WRONLY 比 os.O_TRUNC 更友好,但要注意 rotate 逻辑不能阻塞主流程time.Now().String() 或 runtime.Caller())可能比写入本身还重。