go中跨进程文件锁最可靠方式是用flock(linux/macos)或lockfileex(windows),需基于os.file.fd()对已打开文件加锁,推荐使用github.com/nightlyone/lockfile包自动处理跨平台差异与锁生命周期。

Go 中用 flock 实现跨进程文件锁最可靠
标准库 sync.Mutex 只作用于单个进程内,多进程同时写同一个文件时完全无效。真正起作用的是系统级的 flock(Linux/macOS)或 LockFileEx(Windows),Go 通过 syscall 或封装好的第三方包调用它们。os.File.Fd() 是关键入口——必须基于打开的文件描述符加锁,不能对路径字符串操作。
常见错误是:先 os.OpenFile 再用路径去调 flock,结果锁的是另一个文件实例;或者忘记在 defer 中 Unlock,导致锁残留、后续进程死等。
- 只对已打开的
*os.File调用Lock/Unlock,不是对文件名 - 加锁前确保文件以读写模式打开(
os.O_RDWR),只读打开可能被某些系统拒绝加锁 - Windows 下
flock不可用,需用golang.org/x/sys/windows调LockFileEx,或直接换用兼容包 - 锁是建议性(advisory)的,所有参与进程都得主动检查并遵守,强制性锁(mandatory)在 Linux 上默认关闭且不推荐启用
用 github.com/nightlyone/lockfile 避开 syscall 差异
自己手撸 syscall 跨平台适配太容易翻车:比如 macOS 的 F_SETLK 和 Linux 行为略有差异,Windows 完全另一套 API。直接用成熟封装包更省心,lockfile 包就是专干这事的——它内部自动选 syscall、处理 EBUSY、支持超时,还带 .lock 文件约定。
典型误用是把锁文件路径和业务文件路径搞混。这个包默认在目标路径后加 .lock 后缀,比如传 /data/config.json,它实际锁的是 /data/config.json.lock。如果你手动创建了同名文件,反而会干扰它。
立即学习“go语言免费学习笔记(深入)”;
- 初始化:
lf, err := lockfile.New("/path/to/file"),传入的是业务文件路径,不是锁文件路径 - 阻塞加锁:
err := lf.Lock();非阻塞尝试:err := lf.TryLock()(返回lockfile.ErrBusy) - 务必调用
lf.Unlock(),它会删掉.lock文件;程序 panic 时记得用defer lf.Unlock() - 该包不持有文件句柄,加锁后你仍需自己用
os.OpenFile操作原文件
os.Chmod 和 os.Rename 可能绕过文件锁
文件锁只对「打开-读写-关闭」流程生效。但 Go 常见写法是「写临时文件 → os.Rename 替换原文件」,这招很安全,可它完全不经过原文件的 fd,flock 压根管不了。同样,os.Chmod、os.Remove 这些元数据操作也不触发锁检查。
所以别以为加了锁就万事大吉。如果业务逻辑包含原子替换(如配置热更新)、权限修改或删除重建,锁只保护了「正在写」的那个瞬间,前后动作仍是裸奔状态。
- 若必须原子替换,锁对象应是临时文件所在目录(用
flock锁目录 fd),或改用信号+进程协调 -
os.Rename在同文件系统内是原子的,但不保证锁同步;跨文件系统会变成 copy+remove,风险更高 - 想锁整个操作流程,得把锁范围扩大到包含 rename 前后的所有步骤,而不是只锁 open 的那一刻
并发量大时 flock 性能不是瓶颈,但锁粒度要小心
flock 本身很快,微秒级,不会成为性能热点。真正卡住的是锁竞争:比如所有 worker 进程都去抢同一个日志文件的锁,结果串行写,吞吐归零。这时候不是锁的问题,是设计问题。
容易忽略的一点是:锁的生命周期和业务逻辑耦合太紧。例如一个 HTTP handler 里加锁写文件,但 handler 耗时主要在外部 API 调用,锁却一直占着,白白拖慢其他请求。
- 锁的范围尽量窄——只包住
Write+Sync,别裹进网络请求、数据库查询 - 高频写场景优先考虑分片:按日期生成日志文件,或用
hash(key) % N分散到多个锁文件 - 不要用全局锁保护多个无关文件;每个文件独立锁,避免不必要阻塞
-
fsync是耗时大户,如果允许丢少量日志,可关掉File.Sync(),靠 OS 缓冲
锁机制本身很轻量,麻烦永远出在“以为锁住了就安全”——而忘了锁之外的动作、锁的范围、以及所有参与者是否真的在配合。










