直接用 log.Println 或 file.Write 会写乱文件,因多个 goroutine 共享全局 log.Logger 或无锁 *os.File,导致竞态;唯一内核保障的并发安全方案是 os.O_APPEND;推荐单 writer goroutine + channel 串行化写入。

为什么直接用 log.Println 或 file.Write 会写乱文件
多个 goroutine 同时调用 log.Println 却只写进一个日志文件,不是因为 channel 不行,而是你调用了 log.SetOutput() —— 它修改的是标准库的全局 log.Logger 实例。所有 goroutine 共享这一个对象,最后执行 SetOutput 的那个协程“赢了”,后续所有日志都流向它的 *os.File 句柄。
更隐蔽的问题是:即使每个 goroutine 自己调用 os.OpenFile(..., os.O_WRONLY) 打开同一个路径(如 "app.log"),写入依然可能交错。Linux 下 write 系统调用仅对单次调用保证原子性,但 Go 层的 file.Write([]byte) 可能被拆成多次系统调用;若内容跨缓冲边界,就可能出现 “ReqID:123status:200” 拼成 “ReqID:st200” 这类损坏。
- 根本原因不是 Go 不支持并发,而是
*os.File不带内部锁,也不隔离偏移量 -
os.O_APPEND是唯一由内核保障安全的并发写方案,但仅限追加,不适用于覆盖或随机写 - 不要试图靠
sync.Mutex+file.Seek()做分块写 —— Seek+Write 组合必须全程加锁,否则竞态确定发生
用单 writer goroutine + channel 串行化写入
这是最通用、最可控的方案,尤其适合日志、审计记录、批量导出等高频小数据写入场景。
- 定义带缓冲的 channel:
logCh := make(chan string, 1024),容量按峰值 QPS × 平均延迟预估 - 启动唯一后台 goroutine:
go func() { defer file.Close(); for msg := range logCh { file.WriteString(msg); file.Sync() } }() - 业务 goroutine 只做
logCh <- "[INFO] user=123 login",绝不碰*os.File - 初始化文件时用
os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644),配合sync.Once避免重复打开
注意:file.Sync() 保证落盘,但会拖慢吞吐;若允许少量丢失(如非关键日志),可去掉,改用定时 Flush() 或 buffer 达到阈值后刷盘。
立即学习“go语言免费学习笔记(深入)”;
多文件分片写入比单文件并发更简单可靠
当业务逻辑天然可分片(如按用户 ID、日期、哈希桶),优先让每个 goroutine 写独立文件,彻底规避竞争。
- 生成路径用
filepath.Join("logs", fmt.Sprintf("user_%d.log", userID)),避免硬编码"\" - 每个 goroutine 独立打开、写入、关闭:
file, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); file.Write(data); file.Close() - 合并需求存在时,用
io.Copy或os.Rename在写完后原子合并(先写临时文件,再os.Rename替换) - 需控制 fd 数量?加信号量:
sem := make(chan struct{}, 10),每次写前sem <- struct{}{},写完<-sem
这种模式下,os.O_APPEND 不是必需的,因为每个文件只有单个 writer;但保留它仍可防止意外覆盖。
大文件分块读 + 单 writer 流水线处理
处理单个超大文件(如 >100MB 的 CSV/JSONL)时,并发读取 + 串行写入是常见组合,但分块逻辑必须谨慎。
- 不要共享一个
*os.File句柄让多个 goroutineRead()—— 文件偏移量竞争会导致跳读或 panic - 每个 worker 自己
os.Open(),用file.Seek(offset, io.SeekStart)定位,且Seek()+Read()必须用sync.Mutex包裹 - 更推荐流式分块:主 goroutine 用
bufio.Scanner逐行读,发给jobs chan *LineTask;worker 处理后发结果到results chan []byte;最终由单个 writer goroutine 序列化写入目标文件 - 写入前用
bufio.NewWriterSize(file, 4096)缓冲,减少系统调用次数;注意Writer不是 goroutine 安全的,必须独占
真正容易被忽略的是系统级瓶颈:SSD 上并发 4–8 个 reader 通常最优,HDD 则建议 ≤4;盲目增加 goroutine 数量只会加剧 seek 开销,反而变慢。


















