go中用flock实现跨进程文件写入互斥,需通过unix.lock_ex|lock_nb非阻塞加锁、close自动释放,配合超时重试;它仅提供劝告锁和原子性,不保证写入顺序,也不能替代分布式一致性方案。

Go 里用 flock 实现进程级文件写入互斥,不是靠 sync.Mutex
Go 的 sync.Mutex 只在单个进程内有效,跨进程写同一个文件时完全不起作用。真正防并发冲突得靠系统级文件锁,Linux/macOS 上最直接的就是 flock 系统调用。Go 标准库不直接封装它,得用 golang.org/x/sys/unix 或第三方包如 github.com/gofrs/flock。
常见错误现象:panic: bad file descriptor 或写入内容错乱、部分丢失,往往是因为没检查 flock 返回值,或锁住的是 dup 出来的 fd(flock 是 fd 级别,但只对原始 open 的 fd 生效)。
- 必须在
os.OpenFile后立刻对返回的*os.File的fd调用flock,不能先file.WriteString再加锁 - 使用
github.com/gofrs/flock更安全:它自动处理 fd 复制、close 时自动解锁,且兼容 Windows(用LockFileEx) - 注意
flock是劝告锁(advisory),所有写方都得主动调用才生效;强制锁(mandatory lock)在 Linux 需要挂载选项 + 文件属性,一般不用
flock 加锁失败时阻塞还是立即返回?看 LOCK_EX 和 LOCK_NB 组合
默认 flock(fd, unix.LOCK_EX) 会阻塞,直到锁被释放。线上服务里这很危险——一个卡死的写进程会让后续所有请求 hang 住。必须用非阻塞模式 + 超时控制。
使用场景:日志轮转、计数器更新、配置热写入等需要强一致性的短时写操作。
立即学习“go语言免费学习笔记(深入)”;
- 加锁时用
unix.LOCK_EX | unix.LOCK_NB,失败立刻返回unix.EWOULDBLOCK - 自己实现重试逻辑,比如最多等 200ms,每次间隔 10ms,避免自旋消耗 CPU
- 不要依赖
time.Sleep做“简单重试”,超时精度差,且无法响应 context 取消 - 示例判断:
if err == unix.EWOULDBLOCK { /* 重试 or fallback */ }
Go 中 flock 锁的生命周期:关文件就自动释放,别手动 unix.Flock 解锁
flock 锁绑定在文件描述符上,但释放时机不是调用 unix.Flock(fd, unix.LOCK_UN),而是当该 fd 被 close,或整个进程退出时自动释放。手动解锁不仅多余,还容易出错。
性能/兼容性影响:频繁 open/close 文件再加锁,开销比复用文件句柄大;但复用又要注意锁是否还有效(比如 fork 子进程后,子进程继承 fd,也继承锁状态)。
- 推荐模式:每个写任务单独
os.OpenFile→flock→ 写 →file.Close(),锁随 close 自动释放 - 避免在 long-running goroutine 中长期持有文件句柄和锁,否则可能阻塞其他进程
- Windows 下
flock不可用,gofrs/flock会降级为LockFileEx,行为一致,但路径需用正斜杠或双反斜杠
多个 Go 进程写同一文件时,flock 不保证写入顺序,只保原子性
很多人以为加了 flock 就能按“先到先写”排队,其实不是。flock 只确保同一时刻只有一个进程在写,但不控制谁先抢到锁、谁的数据先落盘。尤其在高并发下,调度不确定性会导致写入顺序和调用顺序不一致。
容易踩的坑:用 flock 实现分布式 ID 计数器、或追加日志时假设顺序,结果发现数字跳变、日志时间戳乱序。
- 若需严格顺序,得额外加序列号或时间戳校验,或改用原子文件替换(write to tmp +
os.Rename) - 追加写(
os.O_APPEND)本身是原子的,但flock+write组合才能防止截断/覆盖,二者要配合用 - 注意:
os.O_APPEND和flock没有隐式关系,必须显式加锁,否则多进程同时 append 仍可能交错
真正难的不是加锁,是想清楚你要保的到底是什么:是“不丢数据”,还是“不错乱”,还是“不重复”。选错了锁策略,后面全是补丁。










