go 1.16+ 应无条件选 filepath.walkdir,它用 io/fs.readdir 避免冗余 stat 调用、快 20%–40%,支持 fs.skipdir 跳过权限不足目录,且需注意 direntry 轻量、不触发 stat。

用 filepath.WalkDir 还是 filepath.Walk?
Go 1.16+ 应该无条件选 filepath.WalkDir。它默认使用 io/fs.ReadDir,避免了 filepath.Walk 那种先 stat 再判断是否为目录的冗余系统调用,在大目录下快 20%–40%,而且能天然跳过权限不足的子目录(返回 fs.SkipDir 错误即可)。
常见错误:有人把 filepath.Walk 的回调函数签名(func(path string, info os.FileInfo, err error) error)直接套到 filepath.WalkDir 上——后者第二个参数是 fs.DirEntry,轻量、不触发 stat,但不能直接读 ModTime() 或 Size()。
- 需要文件大小或修改时间?必须显式调用
entry.Info(),这会触发一次stat - 只匹配文件名?直接用
entry.Name(),零开销 - 想跳过某个目录?在回调里 return
fs.SkipDir,不是nil或errors.New("skip")
文件名匹配该用 strings.Contains 还是 path.Match?
看场景:模糊子串搜“log”“conf”这种,用 strings.Contains(entry.Name(), keyword) 最直白;要支持通配符(如 *.go、test_?.txt),必须用 path.Match(pattern, entry.Name())。
容易踩的坑:path.Match 不支持正则,也不支持 ** 这种双星递归匹配——它只认 *(任意字符)、?(单字符)、[abc](字符集)。想实现 **/*.go?得自己解析 pattern,或换用第三方库如 golang.org/x/exp/filepath(非稳定)。
立即学习“go语言免费学习笔记(深入)”;
- 区分大小写:Windows 默认不区分,Linux/macOS 区分——
strings.Contains是严格字节匹配,path.Match同样严格 - 想忽略大小写搜?用
strings.Contains(strings.ToLower(entry.Name()), strings.ToLower(keyword)) - 匹配前先检查是否为文件:
!entry.IsDir(),否则你会把目录名也当结果返回
怎么安全中断扫描又不 panic?
filepath.WalkDir 的回调函数返回非 nil error 时,会立即停止遍历并把该 error 当作整个调用的返回值。所以别用 panic 或 os.Exit 停止——用户按 Ctrl+C 时应优雅退出,而不是崩掉。
正确做法是传入一个带 cancel 的 context.Context,并在每次回调开头检查 ctx.Err() != nil。但注意:filepath.WalkDir 本身不接受 context,得靠外部控制流。
- 启动 goroutine 执行
WalkDir,主 goroutine 监听os.Interrupt信号,发 cancel - 回调里每处理几百个文件就 select 检查一次
ctx.Done(),及时 returnctx.Err() - 不要在回调里做耗时操作(比如打开每个文件读内容),否则中断响应延迟严重
输出结果顺序为什么和磁盘不一致?
filepath.WalkDir 按操作系统底层目录迭代顺序返回,Linux ext4 通常是哈希乱序,macOS APFS 更不可预测。如果你期望按字母序或时间序展示,必须在收集完所有匹配项后再排序。
性能影响明显:边走边 append 到 slice,最后 sort.Slice(res, func(i, j int) bool { return res[i].Name ,比在回调里反复插入排序快得多。
- 只排文件名?用
strings.Compare(a.Name, b.Name) - 想按修改时间排?得提前调用
entry.Info()并缓存ModTime(),否则排序时再调会重复 stat - 大量结果(>10 万)?考虑用
heap做 top-K,避免全量内存排序
最常被忽略的一点:递归扫描时,符号链接默认会被跟随。如果目标路径存在循环软链(比如 A → B → A),WalkDir 会卡死或爆栈。启动前加一句 filepath.WalkDir 不处理 symlink,得自己在回调里用 entry.Type() & fs.ModeSymlink != 0 跳过,或者用 os.Stat + os.IsSymlink 预检。这事儿没人提醒,但线上跑进死循环就只能 kill -9 了。










