linux/macos 下用 syscall.flock 加写锁需先以 os.o_rdwr 或 os.o_wronly 打开文件,再调用 syscall.flock(fd, syscall.lock_ex|syscall.lock_nb);若用只读模式或未加 lock_nb 易报错或阻塞,且 lock_un 应在 close 前调用。

syscall.Flock 在 Linux/macOS 上怎么加写锁
Linux/macOS 下最轻量的方案就是 syscall.Flock,但它不是自动生效的——必须先用 os.OpenFile 打开文件(且至少带 os.O_RDWR 或 os.O_WRONLY),再对返回的 *os.File 调用 Flock(int(fd.Fd()), syscall.LOCK_EX)。
常见错误包括:
- 用
os.O_RDONLY打开后尝试加LOCK_EX,会直接报operation not supported - 忘记加
syscall.LOCK_NB,导致 goroutine 永久阻塞在锁上(尤其在调试或测试时难察觉) - 在
defer f.Close()之后才调用Flock(..., LOCK_UN),实际锁已在Close()时释放,LOCK_UN变成无效调用
推荐写法:
fd, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
defer fd.Close()
if err = syscall.Flock(int(fd.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
return fmt.Errorf("failed to acquire lock: %w", err)
}
defer syscall.Flock(int(fd.Fd()), syscall.LOCK_UN)
_, _ = fd.WriteString("new line\n")
Windows 下为什么不能用 syscall.Flock
Windows 没有 flock 系统调用,编译时会直接报错:undefined: syscall.Flock。这不是 Go 版本问题,而是平台限制——syscall 包里对应常量和函数在 Windows 构建标签下根本没定义。
可行替代方式只有两种:
- 用
golang.org/x/sys/windows调用LockFileEx:需确保文件句柄通过windows.CreateFile打开(不能用os.OpenFile),且numberOfBytesToLockLow至少为 1,否则返回ERROR_INVALID_PARAMETER - 用第三方库如
github.com/leekchan/goflock或github.com/go-flock/flock:它们内部用//go:build windows和//go:build !windows分发实现,对外统一提供TryLock()接口,省去条件编译胶水代码
注意:Windows 文件锁基于路径而非 inode,若进程 A 锁住 a.txt 后,进程 B 把它重命名为 b.txt,A 的锁依然有效,但新打开的 b.txt 不受该锁约束——这点和 Unix 行为不一致。
sync.Mutex 为什么不能解决多进程冲突
sync.Mutex 完全无效于跨进程场景。它只锁当前进程内的内存变量,对磁盘文件无任何控制力。两个独立运行的 Go 程序(比如两个 ./server 实例)同时写同一个文件,sync.Mutex 彼此完全不可见。
典型误用模式:
- 在包变量声明
var mu sync.Mutex,然后每个写操作前mu.Lock()—— 这只能防止单个二进制内多个 goroutine 冲突 - 把
os.OpenFile放在锁块里,以为“打开+写入”是原子的 —— 实际上另一个进程可能在你OpenFile返回后、Write前就完成了整个写入
真正起作用的永远是内核级锁:flock、LockFileEx、或通过 os.O_APPEND + 单 goroutine channel 消费这种规避竞争的设计。
临时文件 + os.Rename 是不是更可靠
是,而且跨平台、无需系统调用适配。核心逻辑是:每个写入者生成唯一临时文件(如 log_12345.tmp),写完后调用 os.Rename(oldpath, finalpath)。只要源和目标在同个文件系统,Rename 是原子的,不会出现“半截文件”。
但要注意边界条件:
- 临时文件必须和目标文件在同一目录下,否则
os.Rename在多数系统上会退化为 copy+delete,失去原子性 - 如果目标文件已存在,
os.Rename会覆盖它(Unix 行为),Windows 下则可能失败,需提前os.Remove - 无法用于实时追加日志——因为每次都是整份替换,旧内容会丢失;适合批处理、配置更新、快照导出等场景
这个方案的复杂点不在实现,而在于协调:谁负责清理失败的临时文件?是否需要加锁防止两个写入者生成相同临时名?这些都得额外设计,不能只靠 Rename 本身。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











