Go中并发读写同一*os.File不安全,因共享文件偏移量导致数据错乱、覆盖或panic;需用sync.Mutex同步写操作或采用bufio.Writer+单goroutine写模式;并发读虽一般不panic但结果不可预测,应独立打开文件或用io.ReadAt()。

Go 里直接并发读写同一个 *os.File 不安全
多个 goroutine 同时调用 file.Write() 或 file.Read()(尤其是写),会导致数据错乱、覆盖或 panic。底层文件描述符的偏移量(offset)是共享的,而 Write() 默认在当前 offset 写入并移动它——没有内置同步机制。
常见错误现象包括:
- 日志内容被截断或交错(如两行日志混成一行)
-
write /path/to/file: bad file descriptor(极少数情况,因 close 竞态) - 写入字节数少于预期,且无错误返回(偏移量冲突导致部分写入被跳过)
sync.Mutex 能解决并发写,但要注意粒度
对 *os.File 加锁是最直接的方案,但锁的范围和位置影响性能与正确性:
- 必须锁住整个
Write()调用,包括参数准备和错误检查,不能只锁file.Write()本身 - 避免在锁内做耗时操作(如序列化大结构体、网络调用),否则阻塞其他写协程
- 不要用
defer mu.Unlock()在函数开头加锁后直接 defer——容易漏解锁或死锁
示例正确写法:
立即学习“go语言免费学习笔记(深入)”;
mu.Lock()
n, err := file.Write(data)
mu.Unlock()
if err != nil {
// 处理错误
}
高并发场景下优先用 bufio.Writer + 单写 goroutine
比反复加锁更高效:把所有写请求通过 channel 发给一个专属 writer goroutine,由它串行落盘。这样既避免锁竞争,又可批量写入提升 I/O 效率。
-
bufio.Writer的Write()是内存操作,几乎不阻塞 - 需定期
Flush()或设置缓冲区大小(默认 4KB),否则日志可能延迟数秒才写出 - 关闭前必须显式
Close()或Flush(),否则最后一段数据会丢失
关键点:channel 容量要设(如 make(chan []byte, 1024)),防止生产者无限阻塞;写 goroutine 需监听 done 信号确保优雅退出。
并发读文件一般安全,但要注意 Seek() 和共享 offset
多个 goroutine 并发调用 file.Read() 通常不会 panic,但结果不可预测——因为每次 Read() 都基于当前文件 offset,而 offset 是全局共享状态。
- 如果各 goroutine 都想从头读,必须各自
file.Seek(0, 0),且该操作也需同步(否则 Seek 和 Read 之间可能被其他 goroutine 打断) - 更稳妥的做法是每个 goroutine 打开独立的
*os.File(os.Open()),操作系统会为每个 fd 维护独立 offset - 读大文件时,用
io.ReadAt()替代Read()可绕过 offset 管理,但要求文件支持随机读(如普通磁盘文件可以,管道/网络流不行)
真正容易被忽略的是:即使只读,如果文件被其他进程 truncate 或重写,Read() 行为也会异常(比如突然 EOF),Go 不做任何保护。










