直接用os.openfile并发写会丢日志,因为即使带o_append,多个goroutine共享同一文件描述符时,内核无法保证seek+write两步原子性,导致偏移覆盖、行截断或丢失;必须复用单个*os.file并用sync.mutex保护write调用,或改用channel单协程落盘,跨进程则需flock等系统级锁。

为什么直接用 os.OpenFile 并发写会丢日志?
多个 goroutine 同时调用 os.OpenFile 打开同一个文件(即使都带 O_APPEND),底层仍可能复用同一文件描述符,导致 write 系统调用覆盖彼此的偏移位置——write 不是原子的,尤其在小缓冲、高并发下极易丢行或错乱。
这不是 Go 特有,而是 POSIX 文件语义决定的:即使加了 O_APPEND,内核保证的是“追加时自动 seek 到末尾再写”,但多个进程/线程共享 fd 时,seek + write 两步之间仍可能被抢占。
- 现象:日志行数明显少于预期,或出现半截行、乱序
- 错误日志里看不到 panic,但数据已损坏
- 用
lsof -p PID可查到多个 goroutine 共享同一个 fd 号
用 sync.RWMutex 包裹写操作够不够?
够用,但只适用于单进程内并发;它不能跨进程同步,也不解决文件句柄复用问题。关键点在于:锁必须包裹「打开 + 写入 + 关闭」全过程,或更常见地——复用一个长期打开的文件句柄。
推荐做法是全局持有一个 *os.File,所有写请求串行化到该句柄:
- 初始化时用
os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)一次打开 - 用
sync.Mutex(不是 RWMutex)保护file.Write()调用 - 避免在每次写前
OpenFile、写后Close()——开销大且破坏原子追加语义 - 注意:
file.Write本身不保证完整写入,需用io.WriteString或循环检查n, err := file.Write(b)
如何安全支持多进程同时写同一日志文件?
单靠 Go 内存锁无解,必须依赖操作系统级文件锁。Linux/macOS 可用 flock 系统调用,Go 标准库不直接暴露,需借助 golang.org/x/sys/unix 或封装好的包如 github.com/gofrs/flock。
典型流程是:每次写前 acquire 排他锁 → 写 → release。虽然性能比单进程锁低,但能真正防止跨进程冲突:
-
flock是建议性锁(advisory),所有写方必须主动调用才生效,无法阻止恶意进程绕过 - 不要混用
flock和fcntl(F_SETLK),行为不一致 - 务必确保 lock/unlock 成对,panic 时用
defer释放,否则锁可能永久滞留 - 示例:
lock := flock.NewLock("/var/log/app.log")<br>if ok, _ := lock.TryLock(); ok {<br> defer lock.Unlock()<br> file.Write([]byte("log line\n"))<br>}
要不要考虑用 bufio.Writer 提升性能?
可以,但必须和锁配合好。常见错误是把 bufio.Writer 当成线程安全对象——它不是。正确做法是:每个写操作获取锁后,直接调用 wr.Flush() 前先写入缓冲区,或者更稳妥地,不用缓冲,直接 file.Write。
- 如果用了
bufio.NewWriterSize(file, 4096),必须确保所有写都经由该 writer,且每次wr.Write()后调用wr.Flush()(或定期 flush) - flush 失败要处理 error,否则缓冲区内容丢失不报错
- 高吞吐场景下,可考虑每 N 行 flush 一次,但需权衡延迟与可靠性
- 注意:
bufio.Writer的缓冲区在内存中,崩溃时未 flush 的日志必然丢失
真正难的不是加锁,而是确认锁的粒度覆盖了整个写生命周期——从字节生成、编码、写入系统调用,到 fsync 落盘。漏掉任意一环,都可能在压力下暴露问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











