sync.RWMutex无法防止多进程文件冲突,必须使用系统级文件锁(如flock或LockFileEx),否则多实例写同一日志文件必然导致数据丢失、截断或乱码。

直接说结论:Go 标准库的 sync.RWMutex 完全不能防多进程冲突,必须用系统级文件锁(如 flock 或 LockFileEx),否则两个 ./myapp 实例同时写同一个日志文件,100% 丢数据、截断、乱码。
为什么 sync.RWMutex 对文件完全无效
sync.RWMutex 只锁内存变量,不碰磁盘、不跨进程。哪怕你给每个 os.File 都包一层 mu.RLock(),另一个进程照样能 os.OpenFile(..., os.O_WRONLY|os.O_TRUNC, ...) 清空文件——锁根本没传过去。
- 典型现象:A 进程刚
file.Write([]byte("hello"))写一半,B 进程执行file.Truncate(0),A 接着写,结果文件只剩后半截或 EOF - 读 goroutine 正在
io.Copy,另一进程重命名目标文件,读操作直接 panic - 部署多个 Kubernetes Pod 写同一 NFS 文件?
sync.Mutex形同虚设
Linux/macOS 下用 unix.Flock 的正确姿势
别用已弃用的 syscall.Flock,改用 golang.org/x/sys/unix;且必须满足三个硬条件,缺一不可:
- 文件必须用
os.O_RDWR打开(os.O_WRONLY也行),os.O_RDONLY调unix.LOCK_EX直接返回syscall.EBADF - 不能带
os.O_CREATE或os.O_TRUNC—— 否则可能拿到新 inode,锁绑定失效 - 加锁后必须显式
unix.Flock(fd, unix.LOCK_UN),或确保defer f.Close()在锁释放之后执行(推荐先 unlock 再 close)
非阻塞示例:
立即学习“go语言免费学习笔记(深入)”;
file, err := os.OpenFile("config.json", os.O_RDWR, 0644)
if err != nil {
return err
}
defer file.Close()
if err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
if err == unix.EWOULDBLOCK {
return fmt.Errorf("config.json locked by another process")
}
return err
}
// ✅ 此时可安全读写
defer unix.Flock(int(file.Fd()), unix.LOCK_UN)
Windows 下别自己封装 LockFileEx
Windows 没有 flock(2),syscall.Flock 编译就报 undefined: syscall.Flock。自己调 windows.LockFileEx 容易踩坑:
- 锁绑定路径而非 inode,重命名或硬链接后锁失效
-
LOCK_SH在 Windows 上无等效行为,多个进程读同一文件仍需额外协调 -
os.Create创建文件时可能触发windows.ERROR_ACCESS_DENIED
直接用 github.com/gofrs/flock:
- 自动按构建标签分发 Unix/Windows 实现
-
flock.New("/path/to/data.json.lock")—— 锁文件路径独立于业务文件 -
locked, err := fl.TryLock()返回 bool,比 errno 判断更直观 - 权限建议设为
0600,避免其他用户干扰
锁粒度和生命周期最容易被忽略
很多人把整个 HTTP handler 或数据库事务包进锁里,结果并发数掉到 1。真正该锁的只有「打开文件 → 写入 → 关闭」这三步,中间任何耗时操作都得移出去:
- 不要在持有文件锁时做 HTTP 请求、sleep、JSON 解析或复杂计算
- 锁文件本身不能是目标文件(比如别锁
state.yaml自身,应锁state.yaml.lock) - 锁释放后立刻 close 文件,避免 fd 泄漏(尤其在循环写场景)
最常被绕过的点:所有写入方必须统一用同一套锁协议。只要有一个 cron 脚本直接 echo "x" >> log.txt 不加锁,整个机制就崩了——flock 是建议性锁,不强制。


















