sync.rwmutex不能保护文件,仅能保护进程内内存变量;它无法阻止多进程并发写同一文件,导致截断、覆盖或脏读;跨进程文件锁需用flock(unix)或lockfileex(windows),或采用临时文件+rename原子写入方案。

sync.RWMutex 不能保护文件,只能保护内存变量
很多人在多 goroutine 场景下对同一文件做读写,直接套用 sync.RWMutex 加锁,结果发现文件依然被截断、覆盖或读到脏数据。这是因为 sync.RWMutex 是 Go 运行时级的内存锁,只在当前进程内有效,完全不感知磁盘文件,更无法阻止另一个进程(比如 cron 脚本、K8s 新 Pod、手动启动的调试实例)同时打开并写入该文件。
典型错误现象包括:
- 两个进程都成功执行
os.OpenFile(..., os.O_WRONLY|os.O_TRUNC, 0),后者清空文件后前者再写,内容全丢 - 读 goroutine 正读到一半,另一 goroutine 调用
file.Truncate(0)或覆盖写,导致读出乱码或 EOF - 加了
mu.RLock()后仍出现并发写冲突——锁根本没起作用
flock 在 Linux/macOS 上必须用 unix.Flock + O_RDWR 打开文件
Go 标准库没有封装跨进程文件锁,Linux/macOS 下必须用 golang.org/x/sys/unix.Flock,且关键限制是:文件必须以可写方式打开(os.O_RDWR 或 os.O_WRONLY),只读打开(os.O_RDONLY)调用 unix.LOCK_EX 会返回 syscall.EBADF。
正确流程是:
- 先用
os.OpenFile(path, os.O_RDWR, 0)打开已有文件(不要带os.O_CREATE或os.O_TRUNC,否则可能拿到新 inode,锁失效) - 立即调用
unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB)尝试非阻塞加独占锁 - 若返回
unix.EAGAIN,说明锁被占用,需按业务逻辑重试或报错,不能忽略 - 写完后务必显式调用
unix.Flock(int(f.Fd()), unix.LOCK_UN)解锁;defer f.Close()会自动释放锁,但顺序错乱(如先 close 再 unlock)可能导致锁未及时释放
Windows 下不能用 flock,得换 LockFileEx 或用 go-flock
syscall.Flock 在 Windows 上未定义,编译直接失败。Windows 文件锁基于句柄独占语义,等效做法是调用 windows.LockFileEx,但行为差异大:
- 锁绑定路径而非 inode,硬链接或重命名后锁失效
- 无法实现真正的共享读锁(
LOCK_SH),多个进程读同一文件仍需协调 - 若用
os.Create或os.OpenFile(..., os.O_CREATE|os.O_TRUNC, ...),可能触发权限拒绝(windows.ERROR_ACCESS_DENIED)
推荐直接使用 github.com/go-flock/flock,它通过构建标签(//go:build windows / //go:build !windows)自动分发实现,对外统一提供 flock.TryLock() 和 flock.Unlock() 接口,省去平台判断和 fd 管理负担。
临时文件 + rename 才是生产环境最安全的写法
即使加了文件锁,如果写入过程崩溃(panic、OOM、kill -9),锁会随 fd 关闭自动释放,但文件可能已处于中间状态。真正健壮的做法是绕过“锁整个文件”的思路,改用原子性更强的协议:
- 写入临时文件,如
data.json.tmp,路径与目标文件同目录(保证 rename 原子性) - 写完后调用
file.Sync()或fd.Sync()确保数据落盘 - 最后用
os.Rename("data.json.tmp", "data.json")原子覆盖——该操作在绝大多数文件系统上不可中断 - 读端永远只读
data.json,不参与锁,也不关心写过程;rename 完成后,下次读即看到完整新内容
这个模式下,文件锁反而成了多余负担,除非你有强实时性要求(比如必须阻塞读直到写完成)。多数场景中,rename 方案更轻量、更可靠,也更容易测试和 debug。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











