You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何检测Go语言中通过flock加锁的文件是否处于锁定状态?

解决方案

1. 检测文件是否被加锁的实现方法

你使用的flock是内核维护的文件锁,天然支持非阻塞检测能力:持有锁的进程无论任何原因退出,内核都会自动释放该进程持有的所有flock锁,不存在死锁残留的问题。
你只需要在读取进程中对目标文件尝试加非阻塞共享锁(LOCK_SH | LOCK_NB)即可判断锁状态:

  • 加锁成功:证明当前没有任何进程持有该文件的排他写锁,文件处于可用状态
  • 加锁失败且错误为syscall.EWOULDBLOCK:证明当前有活跃进程正在持锁写入,文件未完成
    检测逻辑的示例代码如下:
// CheckFileLock 检测文件是否被加写锁,返回true代表被加锁,false代表无锁,error代表检测异常
func CheckFileLock(filePath string) (bool, error) {
    f, err := os.OpenFile(filePath, os.O_RDONLY, 0400)
    if err != nil {
        if os.IsNotExist(err) {
            return false, nil
        }
        return false, err
    }
    defer f.Close()
    // 尝试加非阻塞共享锁
    err = syscall.Flock(int(f.Fd()), syscall.LOCK_SH|syscall.LOCK_NB)
    if err != nil {
        if err == syscall.EWOULDBLOCK {
            // 加锁失败,当前有排他写锁
            return true, nil
        }
        return false, err
    }
    // 加锁成功,主动释放共享锁
    _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
    return false, nil
}

2. 现有实现的问题修复(匹配半成品文件检测需求)

你当前的实现存在两个逻辑漏洞,会导致无法正确识别异常退出产生的半成品文件:

  • 直接对最终目标文件进行写入和加锁:如果写入进程异常退出,内核会自动释放锁,但半写入的文件还留在路径中,读取进程检测无锁后会把它当成完整文件处理
  • 错误处理缺失:downloadFile中OpenFile的错误被忽略,且io.Copy失败时没有主动删除半成品文件

推荐使用「临时文件+原子重命名」的模式修复,逻辑如下:

  1. 写入时先创建后缀为.tmp的临时文件,对临时文件加写锁
  2. 所有数据写入完成后,原子重命名临时文件为最终的正式文件名
  3. 如果任何步骤出错,直接删除临时文件即可

该方案下只要正式文件名存在,就一定是已经完整写入的文件,不会出现半成品的问题,检测逻辑也更简单。

3. 修复后的完整示例代码

首先调整FileLock的逻辑,增加半成品文件清理方法:

type FileLock struct {
    filePath string
    f        *os.File
}

func (l *FileLock) Lock() error {
    if l.f == nil {
        f, err := os.OpenFile(l.filePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0200)
        if err != nil {
            return err
        }
        l.f = f
    }
    err := syscall.Flock(int(l.f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
    if err != nil {
        _ = l.f.Close()
        _ = os.Remove(l.filePath)
        return fmt.Errorf("cannot flock file %s - %s", l.filePath, err)
    }
    return nil
}

func (l *FileLock) Unlock() error {
    defer func() {
        _ = l.f.Close()
    }()
    return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
}

// Purge 清理锁关联的半成品文件
func (l *FileLock) Purge() error {
    _ = l.Unlock()
    return os.Remove(l.filePath)
}

修改后的downloadFile逻辑:

func downloadFile(response *http.Response, finalPath string) error {
    // 先写临时文件
    tmpPath := finalPath + ".tmp"
    output, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
    if err != nil {
        return err
    }
    localFileLock := &FileLock{filePath: tmpPath, f: output}
    // 加写锁
    if err := localFileLock.Lock(); err != nil {
        return err
    }
    // 写入完成前任何错误都清理临时文件
    defer func() {
        if err := recover(); err != nil {
            _ = localFileLock.Purge()
            panic(err)
        }
    }()
    if _, err := io.Copy(output, response.Body); err != nil {
        _ = localFileLock.Purge()
        return fmt.Errorf("problem saving %s to %s: %s", response.Request.URL, finalPath, err)
    }
    // 刷盘保证数据写入磁盘
    if err := output.Sync(); err != nil {
        _ = localFileLock.Purge()
        return err
    }
    // 解锁
    if err := localFileLock.Unlock(); err != nil {
        _ = localFileLock.Purge()
        return err
    }
    // 原子重命名为正式文件,这一步之后文件才对外可见
    return os.Rename(tmpPath, finalPath)
}

调整后读取进程逻辑可以简化为:

  • 只读取匹配最终格式、没有.tmp后缀的文件即可
  • 如果需要清理异常退出残留的半成品,用前面提供的CheckFileLock方法检测.tmp文件是否被加锁,如果无锁就可以直接删除。

内容的提问来源于stack exchange,提问作者IsaIkari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 10:15:00