直接并发写同一*os.file会出问题,因其write()非原子且共享文件偏移量与缓冲区,即使使用o_append,fmt.fprintln等复合操作仍可能导致内容交错、截断、panic或静默丢数据。

为什么直接让多个 goroutine 写同一个 *os.File 会出问题
因为 *os.File 不是线程安全的——它内部共享文件偏移量、缓冲区和系统调用句柄。哪怕你用了 os.O_APPEND,也不能保证多 goroutine 同时调用 fmt.Fprintln 或 file.WriteString 时不交错。Linux 的 write(2) 系统调用只对单次调用原子,而 Go 层的写操作可能被拆成多次系统调用;两个 goroutine 的日志内容就可能拼成 "ReqID:123st200" 这种损坏结果。
常见现象包括:日志行被截断、顺序错乱、write /path/to/file: bad file descriptor panic、甚至静默丢数据。用 go run -race 跑一下就能看到 Write at 0x... 的竞态报告。
必须先启动消费者 goroutine,再发数据
死锁是最常踩的坑:如果你用无缓冲 channel(make(chan string)),又在启动 printOutput() 之前就调 logCh ,所有生产者会卡在发送处,永远等不到接收方,<code>wg.Wait() 永不返回。
-
正确顺序:先
go printOutput(),再wg.Add()和启动生产者 -
通道必须关闭:生产者结束后要
close(logCh),否则for range logCh永远不会退出 -
不要 defer file.Close() 在消费者里:因为
for range是阻塞循环,defer到函数返回才执行,文件句柄会一直开着
用带缓冲 channel + 单 writer goroutine 是最稳方案
这是 Go CSP 哲学的自然落地:不争抢资源,而是把写入逻辑收束到一个专属 goroutine,其他协程只管发消息。性能、可维护性、错误处理都可控。
- 定义通道:
logCh := make(chan string, 1024),容量按峰值 QPS × 平均延迟预估 - 初始化文件用:
os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - 启动 writer:
go func() { defer f.Close(); for msg := range logCh { _, _ = f.WriteString(msg) } }() - 业务侧只做:
logCh ,绝不碰 <code>*os.File
注意:f.Sync() 能保落盘但拖慢吞吐;非关键日志可去掉,改用定时 flush 或 buffer 达阈值后刷盘。
别信 O_APPEND 是并发安全的万能解
很多资料说“加了 os.O_APPEND 就可以并发写”,这是误导。O_APPEND 只保证每次 write() 的起始位置是 EOF,但不控制多个 goroutine 的执行顺序,也不防止内核缓冲区或 NFS 等特殊文件系统上的交错。实测中依然会出现半截 JSON、空行、零字节块。
真正安全的边界只有一个:**同一时刻,只有一个 goroutine 在调用 Write 或 WriteString**。要么靠 channel 串行化,要么每个 goroutine 写独立文件再合并——没有第三条路。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











