sync.mutex无法解决多进程文件竞争,因其仅作用于单进程内存,对跨进程文件操作无效;应改用flock等跨进程锁或无锁方案如单goroutine写、临时文件原子重命名、双缓冲等。

为什么 sync.Mutex 不能解决多进程文件竞争
它只锁内存,不锁磁盘。两个独立的 ./myapp 进程,哪怕都用了 sync.Mutex,照样会同时打开、截断、覆盖同一文件。典型现象是日志突然变空、配置文件被清零、JSON 写出半截或乱码。这不是锁没加对,而是根本用错了工具——sync.Mutex 的作用域仅限当前进程地址空间,对 fork 出来的子进程、K8s 新调度的 Pod、甚至另一个终端里手动跑的调试实例,完全无效。
flock 在 Linux/macOS 上必须用 os.O_RDWR 打开文件
很多代码直接 os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0) 后调 unix.Flock(fd, unix.LOCK_EX),结果返回 syscall.EBADF。这是因为 unix.Flock 要求 fd 必须可读可写(os.O_RDWR),只读或只写都不行。更隐蔽的是:带 os.O_TRUNC 或 os.O_CREATE 时,若文件不存在会创建新 inode,而锁是绑定 inode 的,旧进程锁着的老文件就白锁了。
- 正确做法:先确保文件存在,用
os.OpenFile(path, os.O_RDWR, 0)打开(不带O_CREATE/O_TRUNC) - 加锁失败时检查错误是否为
unix.EAGAIN(被占用)还是unix.EBADF(打开方式错) -
defer unix.Flock(int(f.Fd()), unix.LOCK_UN)必须在defer f.Close()之前,否则 close 可能提前释放锁
Windows 下别碰 syscall.Flock,直接用 github.com/gofrs/flock
Windows 没有 flock(2) 系统调用,syscall.Flock 编译就报错或运行时返回 ENOSYS。硬写平台判断不仅繁琐,还容易漏掉行为差异:比如 Windows 锁绑定路径而非 inode,重命名后锁失效;也不支持真正的共享读锁(LOCK_SH 会被忽略)。gofrs/flock 自动处理这些,对外统一提供 TryLock() 和 Unlock(),且默认带超时和重试逻辑。
- 锁文件路径应独立于业务文件,例如业务文件是
config.json,锁文件用config.json.lock - 初始化后必须显式调
fileLock.Unlock(),defer最安全 - 权限设为
0600,避免其他用户干扰
并发写文件比加锁更有效的三种替代方案
锁只是兜底手段,真正高吞吐场景下,优先消除共享。比如 Web 日志服务每秒上千请求,单锁排队会让 writer goroutine 成为瓶颈。
- 用
chan []byte+ 单 writer goroutine:所有写请求发字节切片到 channel,由一个 goroutine 顺序落盘,天然无锁,还能批量Write()、加bufio.Writer、控制Flush()频率 - 每个 goroutine 写独立临时文件(用
uuid.NewString()命名),最后os.Rename()原子合并 —— 适合批处理,完全规避锁竞争 - 高频读+低频写场景(如配置缓存),用双缓冲 +
atomic.Value:新配置加载完成后再原子切换指针,读全程无锁,写只在切换瞬间持锁
最容易被忽略的是锁的生命周期:哪怕逻辑上加了锁,只要在临界区内做 http.Get()、time.Now().Format() 或 json.Marshal(),并发吞吐就归零。锁内只做 file.Write() 或 buf.Write(),其他全移出去。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











