os.File.Write 不线程安全,因内核不保证多 goroutine 写入顺序与偏移一致;O_APPEND 仅保障单次系统调用追加,无法覆盖 fmt.Fprintf 等复合操作的竞态;推荐 sync.Mutex + bufio.Writer(显式 Flush)或 channel 单写协程;多进程需 flock 文件锁;务必调用 Sync 或 Flush 防崩溃丢日志。

直接让多个 goroutine 共享一个 *os.File 并调用 Write 或 fmt.Fprintf 是不安全的——即使加了 sync.Mutex,也只解决部分问题;真正健壮的写入必须考虑偏移竞争、缓冲区覆盖、落盘丢失和多进程冲突。
为什么 os.File.Write 不是线程安全的
Go 的 os.File.Write 本身不加锁,也不保证原子性。它只是把字节切片交给系统调用 write(2),而多个 goroutine 并发调用时,内核无法保证它们的写入顺序或偏移位置一致。尤其在非 O_APPEND 模式下,文件指针(offset)会被反复覆盖;即使开了 O_APPEND,格式化类操作(如 fmt.Fprintf)仍可能因内部多次 Write 调用导致行断裂或交错。
- 典型错误现象:
write /path/to/file: bad file descriptor(文件被提前关闭)、日志行缺失、内容重复、某几行被截断拼接 -
O_APPEND只能保证每次write(2)系统调用追加到末尾,但不能保证 Go 层面的复合操作(比如先格式化再写)整体原子 - 不要依赖“测试跑通了就安全”——竞态问题在高并发或低负载切换下才暴露
sync.Mutex + bufio.Writer 是最易落地的方案
对多数中低频写入场景(如调试日志、配置快照),加锁配合缓冲写入能兼顾简单性和可靠性。关键不是“锁住整个写流程”,而是“锁住缓冲区刷新动作”。
- 用
bufio.NewWriter包裹*os.File,避免频繁系统调用 - 每次写前调用
mu.Lock(),写完立刻wr.Flush(),再mu.Unlock() - 必须显式调用
Flush(),否则缓冲区内容可能滞留内存,程序崩溃即丢失 - 不要在 defer 中调用
Flush()——goroutine 退出时 flush 不及时,缓冲区可能堆积
channel 驱动的单写协程才是生产首选
当写入频率高、需保障顺序或要统一格式化(如加时间戳、分级日志),应放弃共享句柄,改用 channel 将所有写请求路由到一个专属 goroutine。这是 Go CSP 哲学的自然体现,不是妥协,而是正解。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 定义
type LogEntry struct { Msg []byte; Level string },用chan LogEntry通信 - 写入协程内独占
*os.File,循环rangechannel,每次file.Write()后可选file.Sync() - channel 缓冲区大小建议设为 100~1000,太小易阻塞生产者,太大则内存占用不可控
- 务必在程序退出前关闭 channel 并等待写入协程结束,否则最后几条日志可能丢弃
多进程写同一文件必须加文件级锁
同一个进程内的 goroutine 可靠同步,不代表多个进程安全。例如 systemd 重启服务、K8s Pod 重建、crontab 定时任务,都可能产生多个进程同时打开同一日志文件。
-
sync.Mutex和 channel 对跨进程无效,必须用操作系统级文件锁 - 推荐用
github.com/go-flock/flock:创建flock.Flock{Path: "/var/log/app.lock"},Lock()成功后再打开并写入目标文件 - 不要用
syscall.Flock手动封装——它在不同 Unix 变种上行为不一致,且 Windows 不支持 - 锁文件路径应与数据文件分离(如
app.log+app.log.lock),避免误删
最容易被忽略的是:无论用哪种同步机制,只要没调用 file.Sync() 或没确保 bufio.Writer.Flush() 成功,程序崩溃后最后一段内容大概率丢失——这不是 bug,是操作系统缓存设计使然。别等线上丢日志才想起加 Sync。

















