如何在Go中判断文件是否被其他程序打开,避免误归档删除?
在Go中可靠检查文件是否被另一个程序打开的方案
问题场景
我用Go开发程序A,需要归档目录下的日志文件,但要排除被程序B正在打开写入的日志文件——程序B会在24小时后关闭这些文件,之后才能归档,两个程序完全独立运行。
当前实现没做文件是否被打开的检查,导致归档了正在被程序B写入的文件;现有逻辑是把文件复制到归档包后删除原文件。我试过用os.Open和os.OpenFile来检查,但都没成功,想知道在归档前如何可靠判断文件是否被另一个Go程序打开?
附现有归档核心代码:
func zipFile(filename string) error { file, err := os.Open(filename) if err != nil { return err } defer func(file *os.File) { err = file.Close() if err != nil { fmt.Println(err) } }(file) fileInfo, err := file.Stat() if err != nil { return err } archiveFile, err := os.Create(fmt.Sprintf("%s.zip", filename)) if err != nil { return err } defer func(archiveFile *os.File) { err = archiveFile.Close() if err != nil { fmt.Println(err) } }(archiveFile) zipWriter := zip.NewWriter(archiveFile) if err != nil { return err } defer func(zipWriter *zip.Writer) { err = zipWriter.Close() if err != nil { fmt.Println(err) } }(zipWriter) writer, err := zipWriter.Create(fileInfo.Name()) if err != nil { return err } _, err = io.Copy(writer, file) if err != nil { return err } err = os.Remove(filename) if err != nil { return err } fmt.Println(fmt.Sprintf("file %s was zipped", fileInfo.Name())) return nil }
可行解决方案
跨进程文件状态检查没有统一的跨平台标准,推荐结合操作系统特性或设计层面的优化来处理:
1. 利用文件独占锁(推荐跨平台方案)
让程序B在打开日志文件时加独占写锁,程序A通过尝试获取锁来判断文件是否被占用:
- 程序B侧:打开文件时添加独占锁,阻止其他进程的写操作或独占锁请求
// 程序B打开日志文件的示例 file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { return err } // Unix平台加独占锁,Windows需用LockFileEx替代 err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX) if err != nil { file.Close() return err } // 程序退出或关闭文件时解锁 defer syscall.Flock(int(file.Fd()), syscall.LOCK_UN) defer file.Close() - 程序A侧:尝试获取共享锁,失败则说明文件被独占占用
嫌平台判断麻烦的话,可以用func isFileInUse(filename string) bool { file, err := os.OpenFile(filename, os.O_RDWR, 0644) if err != nil { return true } defer file.Close() // 非阻塞尝试加共享锁,失败则文件被独占 err = syscall.Flock(int(file.Fd()), syscall.LOCK_SH|syscall.LOCK_NB) if err != nil { return true } // 解锁 syscall.Flock(int(file.Fd()), syscall.LOCK_UN) return false }github.com/gofrs/flock这类跨平台锁库简化实现。
2. 基于时间或命名规则(简单易实现)
既然程序B会24小时后关闭文件,完全可以跳过检查逻辑,直接通过规则筛选可归档文件:
- 时间筛选:只归档最后修改时间超过24小时的文件
func isEligibleForArchive(filename string) (bool, error) { fi, err := os.Stat(filename) if err != nil { return false, err } cutoff := time.Now().Add(-24 * time.Hour) return fi.ModTime().Before(cutoff), nil } - 命名规则:程序B将正在写入的日志命名为
xxx.log.active,关闭后重命名为xxx.log,程序A只归档不带.active后缀的文件。
3. 检查进程打开文件(仅应急方案)
依赖操作系统特性,但稳定性和兼容性较差,不推荐作为核心逻辑:
- Unix/Linux:解析
/proc/<pid>/fd目录或调用lsof命令,但需要进程权限,且可能出现竞态 - Windows:调用
CreateFile并指定非共享参数,失败则说明文件被独占打开,但可能误判只读打开的情况
归档逻辑兜底优化
不管用哪种检查方式,都可能出现检查后文件被程序B打开的竞态问题,建议修改归档删除步骤:
不要直接删除原文件,先原子性重命名为临时文件,再归档临时文件,最后删除临时文件。重命名操作不会影响程序B的写入(程序B会继续写原文件名的新文件):
// 替换原代码中的os.Remove(filename) tmpName := filename + ".tmp" err = os.Rename(filename, tmpName) if err != nil { return err } // 归档完成后删除临时文件,失败可记录日志后续重试 err = os.Remove(tmpName) if err != nil { fmt.Printf("failed to remove tmp file %s: %v\n", tmpName, err) }
内容的提问来源于stack exchange,提问作者Daniel Matute
相关产品推荐
相关产品推荐

