filepath.Walk是最稳妥的递归遍历方式,因其内置处理符号链接循环、权限拒绝等边界情况,且按深度优先稳定遍历;手动递归易漏错导致panic或静默跳过。

用 filepath.Walk 是最稳妥的递归遍历方式
Go 标准库不鼓励手写递归函数遍历目录,filepath.Walk 内部已处理符号链接循环、权限拒绝、路径过长等边界情况,且按深度优先顺序稳定遍历。自己用 os.ReadDir + 递归容易漏掉 os.ErrPermission 处理,导致程序 panic 或静默跳过子目录。
常见错误现象:filepath.Walk 遇到无法访问的子目录(如 /proc/1/fd)时默认继续,但若回调函数返回非 nil 错误(如 errors.New("stop")),整个遍历会提前终止——这点常被忽略,误以为“中断逻辑没生效”。
- 回调函数签名必须是
func(path string, info os.FileInfo, err error) error - 想跳过某个目录?在回调里对
info.IsDir() && info.Name() == "node_modules"返回filepath.SkipDir - 不要在回调里修改传入的
path字符串,它可能被复用;需拷贝再处理
os.ReadDir + 手动递归适合可控场景
当需要精确控制遍历顺序(比如先文件后目录)、或要并发处理子目录(避免阻塞主 goroutine)、或需在进入前预判是否跳过时,os.ReadDir 更灵活。但它不自动处理错误传播,所有 os.ReadDir 调用都必须显式检查 err。
典型坑点:递归调用时传入相对路径(如 "sub/dir"),而 os.ReadDir 只接受绝对路径或相对于当前工作目录的路径——多数情况应拼接为 filepath.Join(root, entry.Name())。
- 递归前先判断
entry.IsDir(),否则对文件调用os.ReadDir会返回not a directory错误 - 并发遍历时注意共享变量竞争,例如统计文件数要用
sync.AtomicInt64或加锁 - Windows 下长路径(>260 字符)需启用 manifest 或用
\\?\前缀,os.ReadDir默认不支持
如何安全过滤和收集结果
遍历目的通常是筛选特定文件(如 *.go)或排除某些目录(如 .git)。直接在回调里做字符串匹配效率低,建议用 path/filepath.Match 或正则预编译模式。注意 filepath.Match 的通配规则与 shell 不同:不支持 **,* 不跨路径分隔符。
收集结果时避免用切片反复 append 导致内存重分配——若大致知道规模(如项目下最多 10k 文件),可预先 make([]string, 0, 10000)。
- 排除
.git目录:在filepath.Walk回调中检测filepath.Base(path) == ".git" && info.IsDir(),返回filepath.SkipDir - 匹配
*.md文件:用matched, _ := filepath.Match("*.md", info.Name()),不要用strings.HasSuffix(忽略大小写时失效) - 路径比较统一用
filepath.Clean(path)归一化,避免./foo和foo被当成不同路径
func walkWithFilter(root string) []string {
var files []string
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
if errors.Is(err, os.ErrPermission) {
return nil // 跳过无权限目录
}
return err
}
if !info.IsDir() {
matched, _ := filepath.Match("*.go", info.Name())
if matched {
files = append(files, path)
}
}
if info.IsDir() && info.Name() == "vendor" {
return filepath.SkipDir
}
return nil
})
return files
}
递归遍历真正的复杂点不在代码行数,而在对错误语义的理解——filepath.SkipDir 和 nil 都不终止遍历,但含义完全不同;os.ErrPermission 必须显式处理,否则可能卡死或静默失败。这些细节不跑真实环境(比如挂载了只读 NFS)根本暴露不出来。










