Go如何递归读取目录文件_目录遍历实现思路说明

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) 归一化,避免 ./foofoo 被当成不同路径
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.SkipDirnil 都不终止遍历,但含义完全不同;os.ErrPermission 必须显式处理,否则可能卡死或静默失败。这些细节不跑真实环境(比如挂载了只读 NFS)根本暴露不出来。