跨平台文件锁应统一用o_create|o_excl创建锁文件,因其原子性可保证互斥;os.file.lock在unix是建议性锁、windows才有效,flock在windows不工作,锁文件方案最稳定可靠。

锁文件被占用时 os.Open 会直接失败,不是“读取未关闭的锁文件”的正确路径
Go 里没有“读取未关闭的锁文件”这种安全操作——锁文件(如 .lock)本身只是个标记,真正起作用的是进程对它的持有(通常是通过 os.OpenFile 配合 syscall.Flock 或 flock 系统调用)。一旦进程崩溃或未释放锁,文件句柄消失,但文件还在磁盘上;此时你 os.Open 它,得到的是普通文件句柄,和原锁毫无关系。
所以,别试图“读取锁内容”来判断状态。真实需求其实是:「如何可靠检测某个锁是否已被其他进程持有?」
- 不要用
os.Stat看文件是否存在——存在 ≠ 被锁定 - 不要用
ioutil.ReadFile读锁文件内容——内容通常为空或仅含 PID,不可靠且无同步语义 - 正确做法是:自己尝试获取同名锁,看是否成功
用 syscall.Flock 检测并争抢文件锁(Linux/macOS)
syscall.Flock 是最轻量、最贴近 POSIX 的方式,适用于单机多进程场景。它不依赖文件内容,只依赖内核级文件描述符锁。
关键点:
- 必须用
os.OpenFile打开文件(不能只os.Create),且 flag 至少含os.O_RDWR或os.O_RDONLY - 调用
syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB):带LOCK_NB才是非阻塞检测 - 返回
syscall.EWOULDBLOCK(Linux)或syscall.EACCES(macOS)表示锁已被占
file, err := os.OpenFile("/tmp/myapp.lock", os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
if err == syscall.EWOULDBLOCK || err == syscall.EACCES {
log.Println("锁已被其他进程持有")
return
}
log.Fatal("flock 失败:", err)
}
// 成功获取锁,可继续执行
Windows 下要用 syscall.LockFileEx,行为差异大
Windows 没有 flock,得用 LockFileEx,且必须打开文件时指定 syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE,否则其他进程根本打不开同一文件——这和 Unix 完全不同。
- 锁范围必须显式指定(哪怕只锁第一个字节),
syscall.LockFileEx不支持“整个文件”简写 - 错误码是
syscall.ERROR_IO_PENDING(异步)或syscall.ERROR_LOCK_VIOLATION(冲突) - Go 标准库不封装 Windows 文件锁,必须手写
syscall调用,容易出错
更稳妥的做法:跨平台项目直接用 github.com/gofrs/flock 这类封装库,它内部自动区分系统并 fallback。
为什么 PID 写入锁文件 + 检查进程是否存活是危险的
很多教程教你在锁文件里写入 os.Getpid(),再读出来用 kill -0 检查进程是否存在。这看似合理,实则充满竞态:
- 写入 PID 后、加锁前,进程可能崩溃 → 锁文件残留但无实际锁
- 检查 PID 存活后、真正加锁前,原进程刚好退出,新进程立即抢占 → 两次检查之间有窗口
-
kill -0在容器或 PID namespace 下可能误判(比如 PID 被复用)
真正的锁必须是原子的:打开文件 + 获取锁,这两个动作在内核层面不可分割。任何基于用户态 PID 的方案都只是妥协,不是替代。
如果你的场景真需要跨机器或高可用,那锁文件本身就错了——该上 Redis 分布式锁或 etcd lease,而不是在本地文件上反复修补。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











