必须用flock:多个独立进程同时写同一文件;不该用:单进程多goroutine写本地文件,此时sync.mutex足够,flock反而引入syscall开销和失败风险。

Go 里文件锁本身不竞争,真正卡住你的是多个进程反复抢同一把 flock,或锁持有时间过长导致请求堆积。 单进程内 goroutine 之间不需要 flock,用 sync.Mutex 就够;跨进程才需系统级文件锁,而它的瓶颈不在 Go 运行时,而在 OS 调度和锁释放时机。
什么时候必须用 flock,什么时候纯属浪费
文件锁是进程粒度的,不是 goroutine 粒度的。如果你的应用是单进程、多 goroutine,并且只操作本地文件(比如写日志、读配置),flock 不仅没用,反而引入 syscall 开销和失败路径(如 EBUSY)。
- 必须用:
多个独立进程同时写同一个文件(如 cron job + web server 共同更新cache.json) - 不该用:
同一进程内多个 goroutine写/tmp/upload-*.bin—— 改用sync.Mutex或分片临时目录即可 - 容易误用:用
flock保护 HTTP handler 中的 DB 查询逻辑 → 锁的是文件描述符,不是数据库连接,完全无效
flock 的 LOCK_EX 和 LOCK_SH 在高并发下的真实行为
syscall.Flock(fd, syscall.LOCK_EX) 是阻塞式独占锁,但“阻塞”不等于“排队公平”。Linux 下它基于等待队列,但无超时、无优先级,一旦有大量进程同时调用,会形成隐式调度热点。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
LOCK_SH允许多个 reader 并发,但只要有一个LOCK_EX在等,新来的LOCK_SH也会被挂起(防止写饥饿被绕过) - 锁释放只发生在
close()或显式syscall.Flock(fd, syscall.LOCK_UN),defer file.Close()是常见但危险的习惯——如果文件提前被其他地方 close,锁就意外丢了 - 没有自动重试机制。遇到
EINTR(系统调用被信号中断)必须手动循环重试,否则锁获取失败却继续执行,数据就乱了
替代 flock 的轻量方案:用原子文件替换代替锁
对“写配置”“存缓存”这类场景,与其让 10 个进程抢一把 flock,不如让每个进程写自己的临时文件,再用 os.Rename 原子替换主文件。Linux/macOS 上 Rename 是原子的,且不阻塞读。
- 读方永远读
config.json,不加锁、不报错、不等待 - 写方生成
config.json.tmp.12345→ 写入 →os.Rename("config.json.tmp.12345", "config.json") - 注意:
os.Rename跨文件系统会失败,确保 tmp 和目标在同 mount point - 配合
os.Chmod保证权限一致,避免因 umask 导致新文件不可读
锁内只做最简操作,别碰 IO 和解析
哪怕用了 flock,锁住的临界区也必须极短。常见错误是把 JSON 解析、HTTP 请求、数据库查询全包进去。
- 正确顺序:
open → flock → read entire file → unlock → parse → validate → (if ok) re-flock → write → unlock → close - 错误做法:
flock → read → json.Unmarshal → http.Post → write → unlock→ 整个过程可能几百毫秒,其他进程全卡在Flock系统调用上 - 若写操作耗时不可控(如加载大 YAML 配置),改用双缓冲:
atomic.StorePointer(&configPtr, unsafe.Pointer(newConfig)),读方用atomic.LoadPointer获取,彻底避开文件锁
文件锁优化的关键,从来不是“怎么加得更稳”,而是“能不能不加”。flock 是最后手段,不是默认选项;原子 rename、进程间通信 channel、共享内存映射,往往比死磕一把系统锁更简单、更可靠。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










