sync.Mutex对文件并发读写无效,因其仅作用于进程内内存变量;必须使用flock等内核级文件锁解决跨进程竞争,且需兼顾锁粒度、生命周期及跨平台兼容性。

Go 语言里,goroutine 之间对同一文件的读写竞争,**不能靠 sync.Mutex 或 sync.RWMutex 解决**——因为这些锁只在当前进程内有效,而文件锁要解决的是**跨进程**的并发访问问题(比如多个 Go 程序实例、或 Go + Python 脚本同时操作一个日志文件)。你得用操作系统级的文件锁。
为什么 sync.Mutex 对文件读写无效
sync.Mutex 只能串行化当前进程内的 goroutine,它对文件描述符本身没有任何控制力。两个独立的 Go 进程打开同一个文件,各自加 sync.Mutex,照样会互相覆盖写入。常见错误现象包括:
- 日志文件内容错乱、行首缺失、部分写入被截断
- 配置文件被多个进程同时
WriteString后变成半截 JSON - 本地缓存文件(如 SQLite 或纯文本)读到脏数据或
EOF异常
这类问题在单测或本地调试时几乎不复现,上线后流量一上来就爆发。
用 syscall.Flock 实现跨进程文件锁
Unix/Linux/macOS 上最轻量、最直接的方式是调用 syscall.Flock,它基于内核的 flock(2) 系统调用,支持共享锁(读)和独占锁(写),且自动随文件描述符关闭而释放。
关键点:
-
syscall.Flock(fd, syscall.LOCK_EX)是写锁,阻塞直到获取成功;加syscall.LOCK_NB可设为非阻塞 -
syscall.Flock(fd, syscall.LOCK_SH)是读锁,允许多个进程共存 - 必须在
os.OpenFile之后、任何 I/O 操作之前调用,且锁绑定在文件描述符上,不是文件路径 - Windows 不支持
flock,需改用syscall.LockFile(本文不展开)
示例:安全追加日志
file, err := os.OpenFile("app.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
panic(err)
}
defer file.Close()
// 加独占锁,防止其他进程同时写
if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
panic(err)
}
defer syscall.Flock(int(file.Fd()), syscall.LOCK_UN) // 必须显式释放
file.WriteString(fmt.Sprintf("[%s] info: done\n", time.Now().Format(time.DateTime)))
goroutine 内部要不要再套 sync.RWMutex?
要,但目的不同:它不是为了保护文件,而是保护**内存中的文件句柄或缓冲状态**。典型场景是“一个文件被多个 goroutine 复用”:
- 多个
goroutine共享同一个*os.File,但各自调用Write—— 此时Write本身不是原子的,可能写乱字节边界 - 你维护了一个全局
map[string]*os.File缓存,goroutine并发查表+打开+缓存,需要保护 map 访问
这时的组合是:
- 外层:
syscall.Flock控制跨进程文件访问 - 内层:
sync.RWMutex控制进程内对文件句柄或缓存结构的并发操作
漏掉任何一层,都可能在高并发或分布式部署下出问题。
容易被忽略的释放时机和错误处理
文件锁不会自动释放,除非文件描述符关闭,或显式调用 LOCK_UN。但这两个动作都不能依赖 defer 在函数退出时统一做——因为:
- 如果加锁后发生 panic,
defer会执行,但若 panic 发生在Write中,而你又没做 recover,程序直接退出,锁其实已随进程终止被内核回收(这是 OK 的) - 更危险的是:你开了文件、加了锁、做了 I/O、然后
return,但忘了defer syscall.Flock(... LOCK_UN)—— 这个锁会一直挂着,直到文件关闭,而如果你把文件句柄长期缓存,锁就一直不放 -
syscall.Flock返回EBADF(fd 无效)或EINTR(被信号中断)必须检查,否则可能误判加锁成功
真正稳妥的做法是:加锁 → 检查 err → I/O → 显式解锁 → 关闭文件。不要省略中间任一环。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











