*os.File 并发写不安全因 POSIX write() 非原子,导致偏移错乱、内容重叠;单进程用 sync.Mutex(锁同一变量、仅包 Write)、多进程须用 flock(advisory,需所有参与者配合),跨平台推荐 go-flock。

为什么直接并发写 *os.File 会出问题
Go 的 *os.File 本身不是并发安全的。它的 Write() 方法内部会修改文件偏移量(offset)和缓冲区状态,多个 goroutine 同时调用会导致写入位置错乱、内容重叠或截断——这不是 Go 的 bug,而是 POSIX 文件语义决定的:内核不保证对同一 fd 的 write() 调用具有原子性。
常见现象包括:
• 日志行被拼接(如 "user logged in" 和 "error: timeout" 写成 "user logged error: timeout")
• 文件末尾出现空字节或重复块
• go run -race 报出 Write at 0x... by goroutine N 竞态警告
sync.Mutex 锁住写操作的正确姿势
关键不是“加锁”,而是“锁什么”和“锁多久”。必须确保所有对同一 *os.File 的写入路径都经过同一把锁,且锁的生命周期严格包裹实际 Write() 调用。
- 锁对象应是包级或结构体字段级变量,避免函数内声明(否则每个 goroutine 拿到的是不同锁)
- 必须用
defer mu.Unlock()配合mu.Lock(),否则 panic 或死锁风险极高 - 不要在锁内做耗时操作:比如
fmt.Sprintf、JSON 编码、time.Now().Format()—— 这些应提前完成,只把最终[]byte交由锁保护写入 - 如果写入逻辑涉及多次
Write()(如 header + body + footer),必须全部包在同一个Lock()/Unlock()块里,不能分段加锁
示例:
var logFile *os.File
var logMu sync.Mutex
func safeLog(msg string) {
logMu.Lock()
defer logMu.Unlock()
logFile.Write([]byte(msg + "\n"))
}
flock 跨进程文件锁为什么比 sync.Mutex 更适合多实例场景
sync.Mutex 只能保护当前进程内的内存变量,完全不感知磁盘文件。当多个进程(如 K8s 多 Pod、cron 脚本、手动调试实例)同时打开并写入同一文件时,sync.Mutex 根本不起作用。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
Linux/macOS 下要用 golang.org/x/sys/unix.Flock,注意:
- 文件必须以
os.O_RDWR或os.O_WRONLY打开;只读打开(os.O_RDONLY)调用unix.LOCK_EX会返回syscall.EBADF - 不能带
os.O_CREATE或os.O_TRUNC,否则可能拿到新 inode,导致锁失效 - 必须显式调用
unix.Flock(fd, unix.LOCK_UN)解锁;defer f.Close()会自动释放锁,但若先Close()再Unlock(),锁可能未及时释放 -
flock是 advisory lock(建议性锁),所有参与者必须主动调用才生效 —— 某个进程绕过加锁直接写,其他进程的锁毫无意义
os.File.Lock() 的坑与替代方案选择
标准库 os.File.Lock() 在不同系统底层行为不一致:Linux/macOS 实际调用 fcntl(进程级、fd 绑定),Windows 调用 LockFileEx(路径绑定、无真正共享锁)。这意味着跨平台不可靠,且它也是 advisory lock。
典型陷阱:
-
file.Lock(syscall.LOCK_EX | syscall.LOCK_NB)才是非阻塞模式;直接传syscall.LOCK_EX会阻塞,直到获取锁 -
defer file.Unlock()在panic、os.Exit或 goroutine 被强制终止时不会执行,锁残留 - 同一路径多次
os.OpenFile会得到不同 fd,彼此锁互不影响 —— “你以为 unlock 了,其实只是关掉了一个句柄”
生产环境推荐用 github.com/go-flock/flock:它通过构建标签自动适配平台,对外统一提供 flock.TryLock() 和 flock.Unlock(),省去平台判断和错误处理胶水代码。
真正难的不是选哪种锁,而是厘清你的并发边界:单进程内 goroutine?多进程共用文件?是否允许其他语言/脚本参与?这些决定了你该用 sync.Mutex、flock,还是干脆换用临时文件 + os.Rename() 原子写入。

















