单进程多goroutine写文件应使用sync.mutex而非syscall.flock,因flock用于跨进程竞争;需细粒度分片加锁、避免锁内io、优先采用原子rename替代文件锁。

单进程多goroutine写文件,别用flock
你根本不需要syscall.Flock。它针对的是跨进程竞争,不是goroutine之间。在单个进程中用flock,只会白耗一次系统调用,还可能遇到EBUSY或EINTR——后者必须手动重试,否则逻辑就崩了。
真正该用的是sync.Mutex或sync.RWMutex(读多写少时):
- 锁粒度要细:比如按文件名哈希分片,每个分片配独立
sync.Mutex,避免所有请求挤一把锁 - 别在锁里做
file.Write:先把内容拼好(如bytes.Buffer),锁内只做map[key] = data这类内存操作,写文件移出临界区 - 如果只是追加日志,考虑用
io.MultiWriter把多个*os.File串起来,由OS保证原子追加(仅限O_APPEND)
多个进程同时写同一文件,才轮到flock上场
syscall.Flock生效的前提是:不同进程打开的是**同一个inode**(即同一文件路径,非硬链接或不同挂载点)。常见误用是cron job和web server各自os.OpenFile("cache.json", ...),但没确认是否真指向同一文件——路径拼错、符号链接跳转、容器挂载差异都可能导致锁失效。
关键实操点:
- 锁必须在
os.OpenFile之后、任何IO之前获取;释放只发生在file.Close()或显式syscall.Flock(fd, syscall.LOCK_UN),defer file.Close()看似稳妥,但如果文件被其他地方提前关了,锁就丢了 - 别用
LOCK_EX保护整个HTTP handler:它锁的是文件描述符,不是业务逻辑。查DB、解析JSON这些完全不该包进去 - 写操作务必短:拿到锁后,只做
json.Marshal+file.Write,别顺手去http.Get上游接口
比flock更轻、更可靠的替代方案:原子rename
对“更新配置”“刷新缓存”这类场景,与其让10个进程抢flock,不如每个进程写自己的临时文件,再用os.Rename原子替换主文件。Linux/macOS上os.Rename是原子的,且读方完全无感知、不阻塞、不报错。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
必须注意的坑:
-
os.Rename跨文件系统会失败:确保临时文件和目标文件在同一个mount点(比如都在/tmp下,别一个在/tmp一个在/var/cache) - 新文件权限可能不对:
os.Rename不继承原文件权限,得跟一句os.Chmod,否则可能因umask导致新文件不可读 - 临时文件名要带进程ID或随机后缀(如
"config.json.tmp." + strconv.Itoa(os.Getpid())),避免并发时重名覆盖
锁内做IO或长耗时操作,等于主动制造瓶颈
pprof里看到runtime.futex占比高,往往不是锁原语慢,而是你让它干了调度器不该管的事。锁本身是内存操作,纳秒级;但一次http.Do或db.Query动辄几十毫秒,这期间所有goroutine全卡在FutexWait上。
典型错误模式:
- 把
json.Unmarshal放在锁里:解析1MB JSON要几毫秒,足够让上百个goroutine排队 - 锁里调
time.Now()再格式化字符串:看似简单,但time.Now()底层有锁,高并发下也成热点 - 用
sync.Mutex保护全局map[string]*User,但每次写都遍历全量做sum:这种聚合操作应退到全局锁,或改用双缓冲+atomic.Value
真正该锁的,只有那行usersMap[key] = user——其余全扔出去。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










