Windows 不支持 syscall.Flock,因其缺失 POSIX flock(2) 系统调用,调用直接返回 ENOSYS;Go 1.19+ 的 os.File.Lock 底层用 LockFileEx 实现强制独占锁,但仅限 Windows,跨平台不可用;gofrs/flock 默认不安全,需显式启用 O_EXCL 和 WithExclusiveCreate();LockFileEx 要求 CreateFile 的 dwShareMode 必须为 0,否则报 Access is denied;锁文件需写入 PID 并检查进程存活,防止僵尸锁。

syscall.Flock 在 Windows 上根本不可用,调用会直接返回 ENOSYS 或 panic,这不是代码写错了,而是系统级缺失。Windows 没有 POSIX 的 flock(2) 系统调用,必须换方案。
为什么 os.File.Lock 能用但又不能全信
从 Go 1.19 开始,os.File.Lock 和 os.File.Unlock 在 Windows 上已稳定支持,底层调用的就是 LockFileEx,能实现真正的强制独占锁;但在 Linux/macOS 上,它目前仍会 panic —— 文档没说清楚,实际是“Windows 专用”。
你如果只跑 Windows,可以用它;但只要有一点跨平台需求(比如本地开发用 macOS、部署到 Windows),就别碰这个 API,否则 CI 直接炸。
别用 github.com/gofrs/flock 默认行为
这个库确实封装了 Windows fallback,但它默认创建锁文件时用的是 os.O_CREATE | os.O_RDWR,没加 O_EXCL。这意味着:两个进程同时调 TryLock,可能都成功打开同一个锁文件,导致“假锁成功”。
必须手动确保锁文件创建带 O_EXCL:
- 自己调
os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600),靠原子性判断是否抢到锁 - 或确认你用的
flock版本 ≥ 0.8.1,并显式传flock.WithExclusiveCreate() - 锁文件路径不能和业务文件相同(比如不要锁
config.json自身),必须另建config.json.lock
LockFileEx 调用时 HANDLE 权限必须为 0
这是 Windows 锁定机制最常踩的坑:LockFileEx 要求文件句柄是以 无共享模式 打开的,即 CreateFile 的 dwShareMode 参数必须为 0(不是 FILE_SHARE_READ 或其它)。
Go 标准库的 os.OpenFile 默认会设 FILE_SHARE_READ | FILE_SHARE_WRITE,所以不能直接传 *os.File 给 LockFileEx —— 必须用 golang.org/x/sys/windows.CreateFile 重开句柄:
在 Windows 11 上"直接发声"的 TTS(从 WSL2/TUI 调用 powershell.exe + System.Speech)。适用于用户说"说出来/读出来/语音播报/用TTS",或反馈"没声音/tts 生成的 mp3 是空的/播不出来",以及需要中文语音但 OpenClaw 内置 tts 不可用时。
h, err := windows.CreateFile(
windows.StringToUTF16Ptr(path),
windows.GENERIC_READ|windows.GENERIC_WRITE,
0, // ← 这里必须是 0,否则 LockFileEx 返回 ERROR_ACCESS_DENIED
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
漏掉这一步,错误信息是 Access is denied,而不是锁被占用,容易误判。
锁文件残留比锁失败更危险
用 O_EXCL 创建锁文件后,进程崩溃或被 kill -9,锁文件不会自动消失。下次启动时会误判“锁被占用”,其实只是个僵尸文件。
必须做两件事:
- 锁文件内容写入
os.Getpid()和时间戳,方便诊断 - 每次尝试获取锁前,先
os.Stat锁文件,读取 PID 并用kill -0(Unix)或OpenProcess(Windows)检查该进程是否还活着
这个检测逻辑没法省——跨进程锁的本质就是状态协调,不是靠一次系统调用就万事大吉。

















