并发写入同一文件会导致数据错乱或panic,必须用sync.mutex串行化或chan异步队列协调;写不同文件虽免锁但有i/o和文件描述符开销。

并发写入同一个文件会直接崩溃
Go 语言中,os.File 本身不是并发安全的——多个 goroutine 同时调用 file.Write() 可能导致数据错乱、覆盖,甚至 panic(比如 write /path/to/file: bad file descriptor)。这不是 Go 的 bug,而是底层系统调用(如 write(2))对同一 fd 并发写入未定义行为。所以「并发写入同一个文件」必须加协调机制,不能裸奔。
用 sync.Mutex 保护单个 *os.File
最直白可靠的方式:所有写操作串行化。适用于写入频率不高、但要求顺序严格或日志追加场景。
关键点:
-
os.OpenFile()要用os.O_APPEND标志,否则Seek+Write在并发下仍可能覆盖 -
Mutex必须保护整个Write过程(含WriteString或WriteAll),不能只锁字符串拼接 - 避免在锁内做耗时操作(如格式化复杂结构体),可提前计算好字节切片
var mu sync.Mutex
f, _ := os.OpenFile("log.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
defer f.Close()
<p>go func() {
mu.Lock()
f.WriteString("[INFO] task1 done\n")
mu.Unlock()
}()</p><p>go func() {
mu.Lock()
f.WriteString("[WARN] task2 slow\n")
mu.Unlock()
}()
</p>
用 chan []byte 做写入队列(推荐用于高吞吐)
比 Mutex 更灵活:写操作异步化,goroutine 只负责发消息,单独一个 writer goroutine 串行落盘。适合日志、批量导出等场景,还能自然支持缓冲、限速、错误重试。
注意细节:
- channel 容量要设(如
make(chan []byte, 1024)),否则生产者可能阻塞 - 写入前最好检查
len(data) > 0,空切片不会报错但浪费调度 - 务必处理
f.Write()返回的n, err,写入失败时别静默丢弃 - 关闭 channel 后,writer 需用
for data := range ch保证消费完再退出
ch := make(chan []byte, 1024)
f, _ := os.OpenFile("out.bin", os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
<p>go func() {
for data := range ch {
if len(data) == 0 { continue }
if _, err := f.Write(data); err != nil {
log.Printf("write failed: %v", err)
}
}
}()</p><p>// 使用
go func() { ch </p><h3>写入不同文件就不用锁?小心磁盘 I/O 瓶颈</h3><p>每个 goroutine 写独立文件(如按 ID 分片:<code>output_123.txt</code>)确实无需同步原语,但不等于无代价:</p>
- 大量小文件会触发频繁的
open(2)/close(2),Linux 下开销显著 - 若同时打开几百个
*os.File,可能触达进程级文件描述符限制(ulimit -n) - SSD 寿命和随机写性能受碎片影响,尤其小块高频写入
实用建议:预打开文件句柄并复用(用 map 缓存 id → *os.File),写完一批后主动 Sync();定期轮转文件名,避免无限创建。
真正难的从来不是“怎么并发”,而是“怎么知道该不该并发”——写入量、延迟容忍度、磁盘类型、错误恢复策略,这些决定了选 Mutex、channel 还是分片。没测过实际吞吐就加 goroutine,大概率换来更慢和更难 debug 的问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











