
本文介绍如何修改 Go 的 filepath.Walk 遍历逻辑,使其在遇到 pagefile.sys、hiberfil.sys 等系统锁定文件时不再中断,而是静默跳过并继续遍历——核心在于正确处理 err 参数,避免将 I/O 错误作为返回值中止遍历。
本文介绍如何修改 go 的 `filepath.walk` 遍历逻辑,使其在遇到 `pagefile.sys`、`hiberfil.sys` 等系统锁定文件时不再中断,而是静默跳过并继续遍历——核心在于正确处理 `err` 参数,避免将 i/o 错误作为返回值中止遍历。
filepath.Walk 的回调函数签名是 func(path string, info os.FileInfo, err error) error,其设计语义明确:仅当返回非 nil 错误时,遍历才会立即终止;若返回 nil,则无论 err 是否为非 nil(例如权限不足、文件被占用),walk 都会尝试继续访问同级或子目录中的其他路径。
原代码中,当 err != nil 时直接执行 fmt.Println(err); return nil,看似无害,但关键问题在于:err 参数本身已代表本次 stat 或 readdir 操作失败(如 pagefile.sys 被系统独占锁住),此时 fi 为 nil,后续 fi.IsDir() 会 panic —— 实际上,原代码因未校验 fi == nil 已隐含崩溃风险;而更隐蔽的错误是,在 err != nil 时仍试图访问 fi.Name() 或 filepath.Dir(fp),这在 fi 为 nil 时必然 panic。
✅ 正确做法是:优先检查 err,有错即跳过当前项,不操作 fi,并始终返回 nil 以保障遍历持续进行。无需全局错误切片,除非你需要事后汇总诊断信息。
以下是修复后的完整示例(精简、健壮、符合 Go 最佳实践):
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
var exts = []string{
"*.psd", "*.cdr", "*.tiff", "*.svg", "*.png", "*.jpeg", "*.jpg",
"*.pdf", "*.txt", "*.rtf", "*.docx", "*.doc", "*.xlsx", "*.xls",
"*.ppt", "*.pptx", "*.accdb", "*.csv", "*.dwg", "*.dxf", "*.dng",
"*.arw", "*.srf", "*.sr2", "*.bay", "*.crw", "*.cr2", "*.dcr",
"*.kdc", "*.erf", "*.mef", "*.mrw", "*.nef", "*.nrw", "*.orf",
"*.raf", "*.raw", "*.rwl", "*.rw2", "*.r3d", "*.ptx", "*.pef", "*.srw",
}
var skipDirs = []string{"WINDOWS", "Program Files"}
func visitFile(path string, info os.FileInfo, err error) error {
// ✅ 第一步:若 stat 失败(如文件被锁定、无权限),跳过处理,继续遍历
if err != nil {
// 可选:记录警告,但绝不 panic 或 return err
// fmt.Printf("Warning: %v
", err)
return nil
}
// ✅ 第二步:跳过目录(只处理文件)
if info.IsDir() {
return nil
}
// ✅ 第三步:检查是否在应跳过的父目录中(注意:使用 filepath.Base(filepath.Dir(...)) 更安全,但此处逻辑可接受)
dir := filepath.Dir(path)
for _, skip := range skipDirs {
if strings.Contains(strings.ToUpper(dir), strings.ToUpper(skip)) {
return nil
}
}
// ✅ 第四步:匹配扩展名(注意:filepath.Match 接收的是 pattern,不是后缀!应改用 strings.HasSuffix 更高效可靠)
name := info.Name()
for _, pattern := range exts {
matched, matchErr := filepath.Match(pattern, name)
if matchErr != nil {
// 模式语法错误属开发期问题,可 panic 或 log.Fatal
fmt.Printf("Invalid pattern '%s': %v
", pattern, matchErr)
continue // 跳过该 pattern,不影响整体
}
if matched {
fmt.Println(path)
break // 找到即停,避免重复输出
}
}
return nil // 始终返回 nil,确保 walk 不中断
}
func getDrives() []string {
var drives []string
for _, c := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
drive := string(c) + ":\"
if _, err := os.Stat(drive); err == nil {
drives = append(drives, string(c))
}
}
return drives
}
func main() {
for _, drive := range getDrives() {
err := filepath.Walk(drive+":\", visitFile)
if err != nil {
// ⚠️ 此处 err 是 walk 整体失败(如根路径不存在),非常罕见
fmt.Printf("Fatal walk error on %s: %v
", drive, err)
}
}
}
? 关键改进说明:
-
安全第一:
err != nil分支中完全不访问info,彻底规避nil pointer dereference; -
语义清晰:所有
return nil明确表达“跳过此项,继续遍历”,符合filepath.Walk设计契约; -
性能优化:扩展名匹配推荐改用
strings.HasSuffix(name, ".pdf")替代filepath.Match(后者用于通配符如*.log),既快又免去错误处理; -
大小写鲁棒:
skipDirs判断使用strings.ToUpper,适配 Windows 路径不区分大小写的特性; -
驱动检测加固:用
os.Stat替代os.Open检查驱动器,更轻量且语义准确。
? 额外建议:
对于生产级文件扫描,可进一步升级为 filepath.WalkDir(Go 1.16+),它使用 fs.DirEntry 减少 stat 调用,性能更高;同时配合 err != fs.SkipDir 实现细粒度目录跳过。但就解决“文件被占用中断”这一核心问题而言,上述 return nil 策略已是简洁、可靠、标准的解决方案。











