os.ReadDir是当前最有效的起点,比filepath.WalkDir快2–5倍,因其返回轻量fs.DirEntry切片、零stat调用,元数据按需加载;需手动递归、控制并发粒度与I/O缓冲策略以避免系统过载。

os.ReadDir 是当前最有效的起点,比 filepath.WalkDir 快 2–5 倍(实测百万小文件目录),关键不在“快”,而在“可控”——它不自动触发 stat,避免无谓系统调用。
为什么不用 filepath.WalkDir 直接扫大文件
它对每个路径都隐式调用 os.Lstat,哪怕你只想要文件名或判断是否为目录。在含数十万子目录的路径下,这会变成 syscall 瓶颈,strace 可见大量 lstat("/path/to/x", ...) 调用。os.ReadDir 返回 fs.DirEntry 列表,元数据按需加载:entry.Name() 和 entry.IsDir() 零开销;只有真要取大小或修改时间时才调 entry.Info()。
怎么用 os.ReadDir 找出 >100MB 的文件
必须手动递归(它不支持深度遍历),但可控制栈/队列结构避免 goroutine 泄漏:
- 用 slice 模拟栈,
append子目录、pop当前路径,避免无限嵌套导致栈溢出 - 只对
entry.IsDir() == true的项递归;对文件直接调entry.Info()拿Size() - 跳过符号链接(
entry.Type()&fs.ModeSymlink != 0),防止循环引用 - 示例逻辑片段:
func findLargeFiles(root string, threshold int64) []string { var large []string stack := []string{root} for len(stack) > 0 { path := stack[len(stack)-1] stack = stack[:len(stack)-1] entries, err := os.ReadDir(path) if err != nil { continue } for _, e := range entries { if e.IsDir() { stack = append(stack, filepath.Join(path, e.Name())) } else { if info, err := e.Info(); err == nil && info.Size() > threshold { large = append(large, filepath.Join(path, e.Name())) } } } } return large }
并发扫描时最常踩的三个坑
不是并发越多越快,而是资源边界卡得越准越稳:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
too many open files:每个os.ReadDir打开一个目录 fd,不限流会快速耗尽 ulimit;用带缓冲 channel(如sem := make(chan struct{}, 16))控制同时打开的目录数 - goroutine 泄漏:不要为每个文件启 goroutine;应为每个**子树根目录**启一个,处理完再退出
- 误用
runtime.GOMAXPROCS:磁盘 I/O 不吃 CPU,调高反而加剧调度抖动;保持默认值(通常等于 CPU 核心数)更稳
大文件本身的内容检查不能靠 os.ReadFile
一读就 OOM 是常态,尤其日志或导出数据文件动辄几 GB:
立即学习“go语言免费学习笔记(深入)”;
- 用
os.Open+bufio.NewReaderSize(f, 64*1024),64KB 缓冲区能显著减少read系统调用次数 - 若只需看文件头(如 magic bytes 判断格式),用
io.ReadFull(f, headerBuf)读前 16 字节即可 - 需要随机跳转查某一行?先构建行偏移索引(顺序扫描一次,记录每行起始
offset),再用f.Seek(offset, io.SeekStart)定位——别把跳表当银弹,元信息才是关键
os.ReadDir 提供了可控入口,但递归方式、并发粒度、I/O 缓冲策略,三者缺一不可。忽略任意一项,都可能让优化变成负优化。

















