go高并发日志写入关键在于选择合适同步方式:低并发用sync.mutex封装writer,中高并发推荐channel驱动,生产环境首选slog或zap;必须使用os.o_append打开文件,否则多实例仍会覆盖日志。

Go 语言高并发日志写入不是“要不要同步”的问题,而是“用什么方式同步才既安全又不拖垮性能”的问题。直接对 *os.File 加 sync.Mutex 能 work,但容易成为瓶颈;盲目复用全局 log 包 + log.SetOutput() 会覆盖输出目标,导致所有日志写进最后一个文件;而裸用 fmt.Println 在多 goroutine 下必然交错。正确解法取决于你的场景权重:简单服务选封装锁,中高并发选 channel 驱动,生产级服务直接上 zap 或 slog。
为什么不能直接用 log.SetOutput() 配多个 goroutine
因为 log.SetOutput() 修改的是标准库 log 包的全局变量 std,所有调用 log.Println() 的地方都共享这个输出目标。当多个 LogWorker 启动时,它们按时间先后调用 log.SetOutput(f),最后那个会把前面的全覆盖掉——结果就是所有日志都进了第四个 worker 的文件,前三个文件空空如也。
- 现象:日志只出现在某个固定文件里,其他文件无内容或只有零星几行
- 根源:不是 channel 没扇出,是
log包状态被反复篡改 - 修复:每个 worker 必须用
log.New()构造独立*log.Logger实例,传入专属io.Writer
用 channel 实现服务化写入(推荐中高并发场景)
这是最符合 Go CSP 哲学的做法:一个 goroutine 专职写文件,其他协程只发消息。天然串行、无锁、易监控、可加缓冲。
- 定义消息结构体:
type LogEntry struct { Message string; Time time.Time } - 启动写入器:
go logWriter("app.log", logCh, doneCh),其中logCh是chan LogEntry - 写入器内部必须用
os.OpenFile(..., os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644),os.Open()只读,会静默失败 - 别忘了加
bufio.Writer缓冲(bufio.NewWriter(f)),小日志量下性能提升明显;记得在退出前writer.Flush()
用 sync.Mutex 封装 Writer(适合低至中并发、快速验证)
如果你只需要让几个 goroutine 安全写同一个文件,且不想引入 channel 调度开销,封装一个带锁的 io.Writer 最直接。
- 定义结构:
type SafeWriter struct { file *os.File; mu sync.Mutex } -
Write()方法内必须先mu.Lock()再file.Write(),否则无效 - 注意:不要把
SafeWriter传给log.New()后再丢给多个 goroutine 共享——它本身是线程安全的,但要确保所有日志都走这个实例,而不是混用fmt.Fprint等其他写法 - 性能瓶颈点在锁争用:QPS 上千后,
mu.Lock()可能排队,此时应切回 channel 方案
直接用 slog 或 zap(生产环境首选)
2026 年了,除非是教学或极简脚本,否则没理由手撸日志同步逻辑。slog(Go 1.21+ 标准库)和 zap 都默认并发安全,且内置结构化、分级、异步写入能力。
-
slog.New(slog.NewJSONHandler(f, nil))返回的*slog.Logger可被任意 goroutine 并发调用Info()/Error() -
zap.NewProduction()默认使用无锁环形缓冲 + 单独写入 goroutine,吞吐远超手写方案 - 关键细节:无论用哪个库,
os.OpenFile的 flag 必须含os.O_APPEND,否则多个进程/实例写同一文件仍可能覆盖(即使库内部已同步)
最容易被忽略的一点:日志文件打开模式比同步逻辑本身还关键。哪怕你用了 zap,如果底层 os.OpenFile 漏了 os.O_APPEND,在多实例部署(比如 Kubernetes 多 Pod 写同一 NFS 卷)时,日志依然会互相截断。别只盯着 goroutine,先盯紧 flag 参数。











