Go 中用 syscall.Flock 实现文件锁最简单可靠,它基于 POSIX 劝告式锁,绑定文件描述符、进程退出自动释放;错误做法如 os.Rename 模拟锁易引发竞态、残留和跨平台问题。

Go 用 flock 实现文件锁最简单可靠
Linux/macOS 下直接调用系统 flock(2) 是最轻量、最符合 POSIX 语义的方案,syscall.Flock 或封装好的 github.com/nightlyone/lockfile 都能用,但别自己造轮子去改文件内容加标记——那不是锁,是竞态温床。
常见错误现象:os.OpenFile 后用 os.Chmod 改权限、或写入临时文件再 os.Rename,这些操作本身不原子,多个进程仍可能同时进入临界区。
-
flock是劝告式锁(advisory),所有参与者必须主动调用才生效;只要你的程序统一用它,就安全 - 锁绑定到文件描述符,进程退出自动释放,不用 defer 或 recover 做兜底(但建议还是加)
- Windows 不支持
flock,若需跨平台,改用github.com/jacobsa/fuse或进程级互斥(如sync.Mutex+ 单例管理器),但后者无法跨进程
syscall.Flock 的典型用法和易错点
别用 os.File.Fd() 后裸调 syscall.Flock 而不检查返回值——失败时 fd 可能已失效,后续写入会 panic 或静默丢数据。
正确姿势:
立即学习“go语言免费学习笔记(深入)”;
file, err := os.OpenFile("data.log", os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
log.Fatal("acquire lock failed:", err) // 如 "resource temporarily unavailable"
}
defer func() {
syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
}()
// 此处写入安全
_, _ = file.Write([]byte("log entry\n"))
-
LOCK_EX是排他锁,LOCK_SH是共享锁(只读场景可用) - 加锁失败默认阻塞,加
syscall.LOCK_NB可非阻塞,返回syscall.EAGAIN或syscall.EWOULDBLOCK - 不要对同一个
*os.File多次Flock:第二次调用会覆盖前一次,不报错但逻辑错乱
为什么不用 os.Rename + 临时文件模拟锁
这种方案看似“无依赖”,实则漏洞明显:两个进程同时 os.Stat 发现锁文件不存在 → 同时 os.Create → 同时 os.Rename,而 Rename 在多数文件系统上对目标路径是原子的,但源路径竞争仍存在,且 Windows 上 Rename 跨卷会失败。
更糟的是,锁文件残留问题:进程崩溃未清理,后续永远卡住。你得额外加超时、PID 检查、心跳续期——这已经是在重写一个分布式锁了。
- 临时文件方案本质是“自定义锁协议”,需要所有写入方严格遵循同一套规则,任意一方跳过就失效
- 没有内核保障,
kill -9后锁状态不可恢复,必须人工干预 - 性能差:每次写入都要多次 syscalls(stat/create/rename/unlink),而
flock一次系统调用搞定
生产环境要注意的边界情况
文件锁不是银弹。如果写入逻辑本身耗时很长(比如压缩大文件后写入),锁持有时间过长会导致其他协程/进程长时间等待,甚至触发超时熔断。这时候该拆的要拆,该异步的要异步。
- 锁粒度尽量细:不要在 HTTP handler 入口就加锁,而是在真正操作文件前一刻加
- 避免在锁内做网络请求、数据库查询等不可控延迟操作
- 日志类场景可考虑先写内存 buffer,定期 flush + flock,减少锁争用
- Docker 容器里注意挂载方式:若用
:ro挂载,flock会失败(只读文件系统不允许加锁)
最常被忽略的一点:锁文件路径必须是同一挂载点下的真实文件。符号链接、NFS、某些容器卷(如 Docker 的 tmpfs)可能不支持 flock,加锁成功但实际不生效。上线前务必在目标环境验证 flock 行为。


















