sync.Mutex对文件并发读写完全无效,因其仅锁进程内内存变量,无法阻止多进程(如多个Pod或cron脚本)同时打开并修改同一文件,导致覆盖、截断或JSON损坏;真正防护需用内核级锁如flock或LockFileEx。

为什么 sync.Mutex 对文件并发读写完全无效
它只锁内存,不锁文件。两个独立进程(比如两个 Pod、两个 cron 脚本)同时打开同一文件,sync.Mutex 彼此毫无感知,照样会覆盖写、截断内容、破坏 JSON 格式。死锁倒不会直接发生,但数据损坏是确定的。
真正要防的是「多进程竞态」,不是「goroutine 间死锁」——这是两类问题。你加 mu.Lock() 后仍看到文件被写乱,说明你误用了锁粒度。
-
sync.Mutex生命周期绑定 Go 进程,进程退出锁就消失;文件锁(如flock)由内核维护,只要 fd 持有,锁就有效 - 跨进程协作必须用内核级锁:
syscall.Flock(Linux/macOS)、LockFileEx(Windows) - 别在
os.OpenFile前加mu.Lock(),那只是让 goroutine 排队打开文件,不阻止其他进程打开
flock 非阻塞调用失败时为何像死锁?
现象是 goroutine 卡住不动、CPU 低、无 panic —— 实际是没加 syscall.LOCK_NB,导致 syscall.Flock 默认阻塞等待,而持有锁的进程已崩溃或忘记释放,它就永远等下去。
这不是 Go runtime 检测到的 fatal error: all goroutines are asleep - deadlock!,而是系统调用层面的无限等待,debug 时容易误判。
立即学习“go语言免费学习笔记(深入)”;
- 务必使用
syscall.LOCK_EX | syscall.LOCK_NB(写锁)或syscall.LOCK_SH | syscall.LOCK_NB(读锁) - 检查返回错误:若
err == syscall.EWOULDBLOCK或err == syscall.EAGAIN,说明锁被占用,需重试或放弃 - 解锁必须在
defer f.Close()之前调用syscall.Flock(int(f.Fd()), syscall.LOCK_UN),否则f.Close()会自动释放,但顺序错乱可能让其他 goroutine 在临界区执行时锁已失
多个 goroutine 共享一个 *os.File 时如何避免 flock 冲突
同一个 *os.File 实例被多个 goroutine 同时调 syscall.Flock,容易因锁状态未同步导致意外解锁或重复上锁。尤其当一个 goroutine 解锁后,另一个还在用该 fd 写,就等于裸奔。
根本解法不是“加更多锁”,而是让文件操作原子化、隔离化:
- 每个需要独占写文件的 goroutine,都应自己
os.OpenFile(带os.O_RDWR),然后立即flock,操作完立刻Close - 不要把
*os.File当成长连接复用——文件句柄不是 TCP 连接,复用反而增加锁管理复杂度 - 若必须复用 fd(如日志轮转场景),确保所有 flock 操作都在同一 goroutine 串行执行,或用
sync.Mutex保护对该 fd 的 flock 调用本身(仅保护系统调用,不保护文件内容) - 读操作可共用
syscall.LOCK_SH,但写操作必须LOCK_EX,且写锁会阻塞后续所有读锁请求
Windows 下如何不写平台特定代码实现等效 flock
Go 标准库没封装 Windows 文件锁,syscall.Flock 在 Windows 上不可用。硬切平台分支会让测试和维护变重,但绕不开。
最简方案是用 golang.org/x/sys/windows 调 LockFileEx,但它要求文件以 FILE_SHARE_READ | FILE_SHARE_WRITE 打开,且锁范围需指定字节偏移(通常传 0 和 math.MaxUint32 锁全文件)。
- Linux/macOS 用
syscall.Flock,Windows 用windows.LockFileEx,二者语义接近但参数差异大 - 别依赖
os.Chmod或临时重命名模拟锁——竞态窗口远大于真实锁,且无法阻止外部进程 - 如果项目允许引入第三方库,
github.com/gofrs/flock封装了跨平台 flock 行为,内部做了 platform switch,但要注意它默认阻塞,仍需手动设超时
文件锁的坑不在 API 多难写,而在「以为加了 sync.Mutex 就安全了」这个认知偏差。真正的边界从来不是 goroutine,而是进程。


















