flock文件锁是最简单可靠的单实例方案,linux/macos直接使用,windows需用createmutex;不依赖进程名或端口,内核自动释放锁,比pid文件更可靠。

用 flock 文件锁是最简单可靠的单实例方案
Linux/macOS 下直接靠文件锁就能拦住第二个进程,Windows 也能用类似思路(但得换 API)。flock 不依赖进程名或端口,不残留死锁,出错时内核自动释放,比 PID 文件靠谱得多。
实操建议:
- 选一个稳定路径做锁文件,比如
/tmp/myapp.lock或同目录下的.lock - 用
os.OpenFile打开锁文件,再调syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB)——LOCK_NB是关键,避免阻塞 - 如果返回
syscall.EAGAIN或syscall.EWOULDBLOCK,说明已被占用,直接os.Exit(1) - 程序退出前记得
syscall.Flock(fd, syscall.LOCK_UN),不过即使忘了,进程结束内核也会清理
注意:不要用 os.Create 后立刻 flock,因为文件可能被其他用户写入;最好用 os.OpenFile 配合 os.O_CREATE|os.O_RDWR,确保权限可控。
Windows 下必须用 CreateMutex 而不是文件锁
Windows 的 flock 行为不一致(尤其 NTFS + SMB),且 Go 标准库没暴露跨平台文件锁接口。实际必须调 Windows API。
实操建议:
- 用
golang.org/x/sys/windows包,调windows.CreateMutex(nil, false, "MyAppUniqueName") - 如果返回非零句柄且
errno == windows.ERROR_ALREADY_EXISTS,说明已有实例 - 名字必须全局唯一,建议带公司/产品前缀,比如
"com.example.myapp.singleinstance" - 不用手动 CloseHandle —— 进程退出系统自动回收,但若想提前释放,可存句柄并调
windows.CloseHandle
别用命名管道或临时文件模拟互斥——容易因权限、UAC、杀软拦截失败。
跨平台封装时别硬写 if/else,用构建标签拆分
混写 runtime.GOOS == "windows" 判断会导致测试难覆盖、编译慢、逻辑缠绕。Go 原生支持按平台分文件。
实操建议:
- 建三个文件:
singleinstance_linux.go(含//go:build !windows)、singleinstance_windows.go(含//go:build windows)、singleinstance.go(公共接口,如Lock() error) - 每个平台文件实现自己的
lockImpl(),统一返回ErrAlreadyRunning错误变量,上层不感知细节 - 测试时用
GOOS=linux go test和GOOS=windows go test分别跑,避免“本地能过 CI 报错”
构建标签比运行时判断更安全——编译期就排除不相关代码,二进制里不会多出无用的 syscall 调用。
别把单实例和健康检查混在一起
有人用端口监听(比如 net.Listen("tcp", ":8080"))来实现单例,这会把「启动排他」和「服务可用性」耦合。一旦端口被占但主进程已死,新实例起不来;反过来,端口空闲但旧实例还在跑,也会误放行。
实操建议:
- 单实例只管「同一时刻最多一个进程存活」,用锁机制;健康检查是另一件事,该单独做 HTTP 探针或心跳文件就做
- 如果真要用端口,至少加超时检测:启动时连自己端口,500ms 内有响应才认为旧实例活着,否则删锁重试——但这又引入竞态,不如纯文件锁干净
- 日志里明确记录是「锁获取失败」还是「端口被占」,排查时少绕三圈
最常被忽略的是锁文件权限和路径可写性——容器环境 /tmp 可能是 tmpfs,但某些 k8s pod 没挂载;systemd 服务默认 WorkingDirectory 是 /,没权限写锁文件。务必在 init 阶段校验路径是否可 open+write。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











