karrick/godirwalk

GitHub: karrick/godirwalk

一个高性能的 Go 语言目录遍历库,通过减少系统调用和优化内存使用,在速度、准确性和易用性上全面超越标准库 filepath.Walk。

Stars: 728 | Forks: 67

# godirwalk `godirwalk` 是一个用于遍历文件系统上目录树的库。 简而言之,我为什么要创建这个库? 1. 它比 `filepath.Walk` 更快。 2. 它在 Windows 上比 `filepath.Walk` 更准确。 3. 它比 `filepath.Walk` 更易于使用。 4. 它比 `filepath.Walk` 更灵活。 根据您的具体情况,[您可能不再需要用于 Go 中遍历文件的库](https://engineering.kablamo.com.au/posts/2021/quick-comparison-between-go-file-walk-implementations)。 ## 用法示例 `examples/` 子目录中提供了额外的示例。 该库会通过在其第一个参数上调用 `filepath.Clean`,根据特定于操作系统的路径分隔符来规范化提供的顶级目录名。然而,在调用提供的回调函数时,它总是提供使用正确的特定于操作系统的路径分隔符创建的路径名。 ``` dirname := "some/directory/root" err := godirwalk.Walk(dirname, &godirwalk.Options{ Callback: func(osPathname string, de *godirwalk.Dirent) error { // Following string operation is not most performant way // of doing this, but common enough to warrant a simple // example here: if strings.Contains(osPathname, ".git") { return godirwalk.SkipThis } fmt.Printf("%s %s\n", de.ModeType(), osPathname) return nil }, Unsorted: true, // (optional) set true for faster yet non-deterministic enumeration (see godoc) }) ``` 该库不仅提供了遍历文件系统目录树的函数,还提供了获取特定目录直接子级列表的函数,通常比使用 `os.ReadDir` 或 `os.ReadDirnames` 快得多。 ## 描述 以下是我为什么优先使用 `godirwalk` 而不是 `filepath.Walk`、`os.ReadDir` 和 `os.ReadDirnames` 的原因。 ### 它比 `filepath.Walk` 更快 在与 `filepath.Walk` 的基准测试对比中,观察到它在 darwin 上的运行速度是后者的 5 到 10 倍,速度与 unix 的 `find` 实用程序相当;在 linux 上大约快两倍;在 Windows 上大约快四倍。 它是如何获得这种性能提升的?它做的工作更少,却为您提供几乎相同的输出。该库调用相同的 `syscall` 函数来完成工作,但它进行的调用更少,不会丢弃可能需要的信息,并且通过重用相同的临时缓冲区从目录中读取数据,而不是每次从操作系统读取文件系统条目数据时重新分配新缓冲区,从而减少了过程中的内存波动。 在遍历文件系统目录树时,`filepath.Walk` 获取目录的直接子级列表,并丢弃操作系统提供的与节点名称一起附带的文件系统条目的节点类型信息。然后,在调用回调函数之前,`filepath.Walk` 会对每个节点调用 `os.Stat`,并将返回的 `os.FileInfo` 信息传递给回调。 虽然 `os.Stat` 提供的 `os.FileInfo` 信息非常有用——甚至包含了 `os.FileMode` 数据——但提供它需要对每个节点进行额外的系统调用。 因为大多数回调只关心节点类型是什么,所以该库不会丢弃类型信息,而是以 `os.FileMode` 值的形式将该信息提供给回调函数。请注意,该库提供的 `os.FileMode` 值仅包含节点类型信息,而不包含文件模式中的权限位、粘滞位或其他信息。如果回调确实关心特定节点的整个 `os.FileInfo` 数据结构,回调可以在需要时且仅在需要时轻松调用 `os.Stat`。 #### 基准测试 ##### macOS ``` $ go test -bench=. -benchmem goos: darwin goarch: amd64 pkg: github.com/karrick/godirwalk BenchmarkReadDirnamesStandardLibrary-12 50000 26250 ns/op 10360 B/op 16 allocs/op BenchmarkReadDirnamesThisLibrary-12 50000 24372 ns/op 5064 B/op 20 allocs/op BenchmarkFilepathWalk-12 1 1099524875 ns/op 228415912 B/op 416952 allocs/op BenchmarkGodirwalk-12 2 526754589 ns/op 103110464 B/op 451442 allocs/op BenchmarkGodirwalkUnsorted-12 3 509219296 ns/op 100751400 B/op 378800 allocs/op BenchmarkFlameGraphFilepathWalk-12 1 7478618820 ns/op 2284138176 B/op 4169453 allocs/op BenchmarkFlameGraphGodirwalk-12 1 4977264058 ns/op 1031105328 B/op 4514423 allocs/op PASS ok github.com/karrick/godirwalk 21.219s ``` ##### Linux ``` $ go test -bench=. -benchmem goos: linux goarch: amd64 pkg: github.com/karrick/godirwalk BenchmarkReadDirnamesStandardLibrary-12 100000 15458 ns/op 10360 B/op 16 allocs/op BenchmarkReadDirnamesThisLibrary-12 100000 14646 ns/op 5064 B/op 20 allocs/op BenchmarkFilepathWalk-12 2 631034745 ns/op 228210216 B/op 416939 allocs/op BenchmarkGodirwalk-12 3 358714883 ns/op 102988664 B/op 451437 allocs/op BenchmarkGodirwalkUnsorted-12 3 355363915 ns/op 100629234 B/op 378796 allocs/op BenchmarkFlameGraphFilepathWalk-12 1 6086913991 ns/op 2282104720 B/op 4169417 allocs/op BenchmarkFlameGraphGodirwalk-12 1 3456398824 ns/op 1029886400 B/op 4514373 allocs/op PASS ok github.com/karrick/godirwalk 19.179s ``` ### 它在 Windows 上比 `filepath.Walk` 更准确 我以前也不关心这个,但请听我说。我们都喜欢“一次编写,到处运行”。对于我们创建的软件能够在 Go 支持的所有架构和操作系统上不加修改地运行,这对于该语言的采用、发展和成功至关重要。 当被遍历的文件系统存在由指向目录的符号链接引起的逻辑循环时,在 unix 上 `filepath.Walk` 会忽略符号链接并无误地遍历整个目录树。然而在 Windows 上,`filepath.Walk` 会继续跟随目录符号链接,即使它不应该这样做,最终在将无尽的符号链接循环拼接到路径名中导致路径名过长时,导致 `filepath.Walk` 提前终止并返回错误。此错误来自 Windows,穿过 `filepath.Walk`,传递给运行 `filepath.Walk` 的上游客户端。 关键在于,`filepath.Walk` 的行为会因其运行的平台而异。虽然这显然不是有意的,但在标准库中修复它之前,它会带来兼容性问题。 ### 它比 `filepath.Walk` 更易于使用 虽然该库力求模仿编写极其出色的 `filepath.Walk` 标准库的行为,但在某些地方它为了提供更简单或更直观的调用者接口而略有偏离。 #### 回调接口不会发送供您检查的错误 由于此库不会对它遇到的每个文件系统节点调用 `os.Stat`,因此回调函数不可能存在可过滤的错误事件。`filepath.WalkFunc` 函数签名中用于将 `os.Stat` 的错误传递给回调函数的第三个参数不再是必需的,因此在该库的回调函数签名中被删除了。 此外,`filepath.WalkFunc` 与此库的 `WalkFunc` 之间的这种微小接口差异消除了回调处理程序在使用 `filepath.Walk` 时必须编写的样板代码。除了每个回调函数都需要检查传入的错误值并相应地分支处理之外,该库的用户在进入回调函数时甚至根本不需要检查错误值。这在运行时性能和代码清晰度上都是一种改进。 #### 使用操作系统特定的文件系统路径分隔符调用回调函数 在每个操作系统平台上,`filepath.Walk` 都会使用正斜杠(`/`)分隔的路径名来调用回调函数。相比之下,此库使用特定于操作系统的路径分隔符来调用回调,从而避免了回调函数在实际使用所提供的路径名之前必须为每个节点调用 `filepath.Clean`。 换句话说,即使在 Windows 上,`filepath.Walk` 也会使用 `some/path/to/foo.txt` 调用回调,这就要求编写良好的客户端在处理指定文件之前对每个文件执行路径名规范化。这是创建真正与操作系统无关的回调函数的一个隐藏的样板要求。实际上,许多在 unix 上开发且未在 Windows 上测试的客户端忽略了这一细微差别,当有人尝试在 Windows 上运行该软件时会导致软件错误。 在 Windows 上运行时,此库会对同一文件使用 `some\path\to\foo.txt` 调用回调,从而消除了客户端规范化路径名的需要,并降低了客户端能在 unix 上运行却不能在 Windows 上运行的可能性。 此增强功能消除了回调函数中更多样板代码的必要性,同时提高了该库的运行时性能。 #### `godirwalk.SkipThis` 比 `filepath.SkipDir` 更直观易用 此库必须模拟的 `filepath.WalkFunc` 接口中一个 arguably 容易引起混淆的方面是,调用者如何告诉 `Walk` 函数跳过文件系统条目。对于 `filepath.Walk` 和此库的 `Walk`,当回调函数想要跳过一个目录并且不进入其子级时,它会返回 `filepath.SkipDir`。如果回调函数对非目录返回 `filepath.SkipDir`,`filepath.Walk` 和此库将停止处理当前目录中的更多条目。这并不一定是大多数开发人员想要或期望的。如果您只想跳过特定的非目录条目但继续处理目录中的条目,回调函数必须返回 nil。 这种接口设计的含义是,当您想要遍历文件系统层次结构并跳过一个条目时,您必须根据该节点是哪种类型的文件系统条目返回不同的值。要跳过一个条目,如果该条目是目录,则必须返回 `filepath.SkipDir`;如果该条目不是目录,则必须返回 `nil`。这是我一直观察到许多开发人员在苦苦挣扎的一个不幸障碍,仅仅是因为它不是一个直观的接口。 以下是一个遵循 `filepath.WalkFunc` 接口的回调函数示例,用于让其跳过任何完整路径名包含特定子字符串 `optSkip` 的文件系统条目。请注意,当回调函数返回 `filepath.SkipDir` 时,此库仍然支持与 `filepath.Walk` 相同的行为。 ``` func callback1(osPathname string, de *godirwalk.Dirent) error { if optSkip != "" && strings.Contains(osPathname, optSkip) { if b, err := de.IsDirOrSymlinkToDir(); b == true && err == nil { return filepath.SkipDir } return nil } // Process file like normal... return nil } ``` 通过提供一个新的令牌错误值 `SkipThis`,此库试图消除回调函数中必需的一些逻辑样板,回调函数可以返回该值以跳过当前文件系统条目,无论它是什么类型的条目。如果当前条目是目录,则不会枚举其子级,这与回调返回 `filepath.SkipDir` 完全一样。如果当前条目是非目录,则将枚举当前目录中的下一个文件系统条目,这与回调返回 `nil` 完全一样。以下示例回调函数与前者具有相同的行为,但样板代码更少,而且诚然,其逻辑我觉得更容易理解。 ``` func callback2(osPathname string, de *godirwalk.Dirent) error { if optSkip != "" && strings.Contains(osPathname, optSkip) { return godirwalk.SkipThis } // Process file like normal... return nil } ``` ### 它比 `filepath.Walk` 更灵活 #### 可配置的符号链接处理 此库的默认行为是在遍历目录树时忽略指向目录的符号链接,就像 `filepath.Walk` 所做的那样。但是,它确实会使用它找到的每个节点(包括符号链接)来调用回调函数。如果存在在遍历目录树时跟随符号链接的特定用例,可以通过将 `FollowSymbolicLinks` 配置参数设置为 `true` 来调用此库以执行此操作。 #### 可配置的目录子级排序 此库的默认行为始终是在访问每个节点之前对目录的直接子级进行排序,就像 `filepath.Walk` 所做的那样。这通常是期望的行为。然而,当目录节点具有大量条目时,这确实会带来轻微的性能和内存损失,因为需要对名称进行排序。此外,如果调用者在配置参数中指定了 `Unsorted` 枚举,则在调用者使用条目时会延迟执行读取目录操作。如果存在不需要在访问其节点之前对目录的直接子级进行排序的特定用例,则当 `Unsorted` 参数设置为 `true` 时,此库将跳过排序步骤。 这里有一篇有趣的文章,讲述了以非确定性顺序遍历文件系统层次结构的潜在危险。如果您知道您正在解决的问题不受文件访问顺序的影响,那么我鼓励您使用 `Unsorted`。否则,请跳过设置此选项。 [研究人员发现 Python 脚本中的 bug 可能已经影响了数百项研究](https://arstechnica.com/information-technology/2019/10/chemists-discover-cross-platform-python-scripts-not-so-cross-platform/) #### 可配置的子级后置回调 此库为上游代码提供了指定回调函数的能力,该函数将在每个目录的子级被处理后被调用。这已被用于在以更有效的方式遍历文件系统后递归删除空目录。有关此用法的示例,请参见 `examples/clean-empties` 目录。 #### 可配置的错误回调 此库为上游代码提供了指定回调的能力,该回调将在操作系统返回错误时被调用,从而允许上游代码确定要采取的下一个行动方案,是停止遍历层次结构(就像没有提供错误回调时那样做),还是跳过导致错误的节点。有关此用法的示例,请参见 `examples/walk-fast` 目录。
标签:EVTX分析, Go, Ruby工具, SOC Prime, 开发工具, 文件系统, 日志审计