os.File.Lock()不可靠,因跨平台语义不一致、仅建议性锁、不保证跨进程互斥且异常时易残留;应改用flock库或手写unix.Flock并严守约束。

直接用 os.File.Lock() 做部署锁不可靠,它在不同系统底层行为不一致,且 Windows 下不支持共享锁、Linux/macOS 下又不保证跨进程互斥——部署场景下一旦失效,轻则覆盖配置,重则服务中断。
为什么标准库 os.File.Lock() 不能用于部署锁
这个方法看似方便,但实际是「假跨平台」:Linux/macOS 调用 fcntl()(基于 fd),Windows 调用 LockFileEx()(基于句柄),而两者语义差异极大:
-
fcntl锁随文件描述符关闭自动释放,但父子进程不继承;LockFileEx锁绑定路径,硬链接或重命名后锁就失效 - Windows 完全不支持
LOCK_SH,意味着你没法做「只读检查 + 写入准备」这种部署常见流程 - 更关键的是:
os.File.Lock()是建议性锁,且只对当前*os.File实例生效;如果另一个部署脚本用os.OpenFile("config.yaml", ...)新开一个 fd,它完全感知不到你在锁什么 - 部署失败时若进程被
kill -9或os.Exit()中断,defer file.Unlock()根本不执行,锁残留导致后续所有部署卡死
用 github.com/go-flock/flock 实现可靠部署锁
这个库通过构建标签自动分发 Unix 和 Windows 实现,对外统一提供 flock.TryLock() 和 flock.Unlock(),屏蔽了系统差异,适合部署脚本这种「一次运行、必须成功或明确失败」的场景:
- 锁文件路径必须是稳定、可预测的(如
/var/run/myapp-deploy.lock),不能依赖临时目录或随机名 - 调用
f.TryLock()后必须检查返回值 ——locked == false表示已被占用,应立刻退出并报错,不要重试(部署不是轮询任务) -
f.Unlock()必须在主逻辑结束前显式调用,不要依赖进程退出自动释放;它内部会尝试清理锁文件(如果存在) - 锁文件本身不应包含业务数据,只作协调用途;避免和配置文件共用同一路径(比如
config.yaml.lock),防止权限或 SELinux 策略干扰
示例片段:
立即学习“go语言免费学习笔记(深入)”;
f := flock.New("/var/run/myapp-deploy.lock")
locked, err := f.TryLock()
if err != nil {
log.Fatal("锁初始化失败:", err)
}
if !locked {
log.Fatal("部署锁已被占用,请检查是否已有进行中的部署")
}
defer f.Unlock()
// ✅ 此处可安全执行:解压、校验、替换配置、reload systemd 等
不用第三方库时,Unix 系统上手写 unix.Flock 的要点
如果你因合规或极简要求不能引入外部依赖,Linux/macOS 可直接用 golang.org/x/sys/unix,但必须严格遵循以下约束:
- 文件必须用
os.O_RDWR打开(不能只读或只写),否则unix.Flock(fd, unix.LOCK_EX)在 macOS 上会返回EBADF - 不能用
os.O_CREATE | os.O_TRUNC—— 这会导致每次打开都可能拿到新 inode,旧锁还在原文件上,新进程根本锁不住 - 加锁失败时判断错误是否为
unix.EWOULDBLOCK或unix.EAGAIN,其他错误(如EBUSY、EINVAL)要立即中止部署,不能忽略 - 解锁必须显式调用
unix.Flock(fd, unix.LOCK_UN),不能只靠defer f.Close();因为Close()只是关闭 fd,内核虽通常会释放锁,但不保证顺序及时性
部署锁最容易被忽略的三个细节
锁本身只是手段,真正决定成败的是上下文控制:
- 锁文件权限必须设为
0600或至少0644,避免非部署用户误删或覆盖;但不要设成0444(只读),否则Unlock()会失败 - 部署脚本启动后应尽快获取锁,而不是在解压完、校验完、生成配置后再去抢锁——中间空窗期就是竞态窗口
- 如果部署涉及多个文件(如 config + binary + systemd unit),锁只保护协调点,具体写入仍需原子操作(如先写
config.new,再os.Rename()覆盖);锁不等于写入安全


















