只能通过非阻塞试锁判断:linux/macos用flock(fd, lock_ex|lock_nb),失败且errno==ewouldblock说明被占用;windows需用createfile配file_share_read,失败且getlasterror()==error_sharing_violation说明被写打开。

如何用 flock 判断文件是否被占用
不能直接“查询”一个文件是否被加锁,flock 是劝告锁(advisory lock),内核不维护全局锁状态表,也没有类似 isLocked() 的系统调用。唯一可靠的方式是:尝试以非阻塞方式加独占锁(LOCK_EX|LOCK_NB),根据返回错误判断。
常见错误现象包括:
-
unix.EWOULDBLOCK或unix.EAGAIN:说明文件已被其他进程持锁,当前无法获取 -
syscall.EBADF:文件是以os.O_RDONLY打开的,flock不允许只读 fd 上加写锁 -
syscall.ENOSYS:Windows 系统下syscall.Flock未实现,必须换用windows.LockFileEx
所以判断逻辑本质是“试锁”,不是“查锁”。不要试图绕过这个步骤去读取锁文件元数据或检查进程列表——那些都不可靠且易失效。
TryLockWithTimeout 的轮询实现要点
Go 没有原生超时参数支持 flock,必须手动轮询。关键不是“怎么写 for 循环”,而是控制好重试节奏和资源释放边界。
实操建议:
- 使用
time.NewTicker(50 * time.Millisecond)而非time.Sleep,便于配合select响应ctx.Done() - 每次轮询前必须确保
*os.File是以os.O_RDWR打开的;若只读打开,unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB)会返回unix.EBADF - 不要在
defer里调用unix.Flock(..., unix.LOCK_UN)—— 锁由 fd 关闭自动释放,提前 unlock 可能触发bad file descriptor - Windows 下必须用
windows.LockFileEx,且需将*os.File转为syscall.Handle,直接用.Fd()会返回ERROR_INVALID_HANDLE
为什么不能复用 *os.File 句柄做多次锁等待
多个 goroutine 共享同一个 *os.File 并反复调用 flock,看似省事,实则埋雷。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
原因很实际:
- 锁绑定在文件描述符(fd)上,不是绑定在 Go 对象上;
file.Close()后 fd 失效,再调用f.Fd()返回的是 -1,后续flock必 panic - fork 子进程会继承父进程的 fd 和锁状态,可能导致子进程意外持有锁,父进程却认为已释放
- dup 出来的 fd 不继承锁,但你很难保证所有代码路径都用了原始 fd
正确做法是:每个锁等待任务独立 os.OpenFile(path, os.O_RDWR, 0) → 尝试加锁 → 写/读 → file.Close()。哪怕只是判断是否被占用,也该走完整流程。
第三方库 github.com/go-flock/flock 的真实代价
它确实封装了平台差异、fd 管理和超时轮询,API 简洁:f := flock.New("path"); f.TryLockWithContext(ctx)。但别误以为它“解决了根本问题”。
要注意的隐含限制:
- 仍是劝告锁:只要有一个进程跳过
f.TryLock()直接os.WriteFile,整个锁机制就形同虚设 - 默认无限等待:必须显式传入带 timeout 的
context.Context,否则TryLock()会一直阻塞 - 内部仍用轮询:每 200ms 尝试一次
flock或LockFileEx,高频等待场景下 syscall 开销可观 - 锁文件路径后自动加
.lo后缀:若业务依赖精确路径(如日志轮转、配置热加载),这点容易引发路径不一致 bug
真正复杂的点从来不在“怎么加锁”,而在于“所有参与者是否统一走同一套锁协议”——漏掉一个 cron 脚本、一个 shell 临时命令、一个没升级的旧版本二进制,整套机制就崩了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










