Fast directory traversal for Golang
godirwalkis a library for traversing a directory tree on a file system.
In short, why do I use this library?
filepath.Walk.
filepath.Walk.
filepath.Walk.
filepath.Walk.
Additional examples are provided in the
examples/subdirectory.
This library will normalize the provided top level directory name based on the os-specific path separator by calling
filepath.Cleanon its first argument. However it always provides the pathname created by using the correct os-specific path separator when invoking the provided callback function.
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) })
This library not only provides functions for traversing a file system directory tree, but also for obtaining a list of immediate descendants of a particular directory, typically much more quickly than using
os.ReadDiror
os.ReadDirnames.
Here's why I use
godirwalkin preference to
filepath.Walk,
os.ReadDir, and
os.ReadDirnames.
filepath.Walk
When compared against
filepath.Walkin benchmarks, it has been observed to run between five and ten times the speed on darwin, at speeds comparable to the that of the unix
findutility; and about twice the speed on linux; and about four times the speed on Windows.
How does it obtain this performance boost? It does less work to give you nearly the same output. This library calls the same
syscallfunctions to do the work, but it makes fewer calls, does not throw away information that it might need, and creates less memory churn along the way by reusing the same scratch buffer for reading from a directory rather than reallocating a new buffer every time it reads file system entry data from the operating system.
While traversing a file system directory tree,
filepath.Walkobtains the list of immediate descendants of a directory, and throws away the node type information for the file system entry that is provided by the operating system that comes with the node's name. Then, immediately prior to invoking the callback function,
filepath.Walkinvokes
os.Statfor each node, and passes the returned
os.FileInfoinformation to the callback.
While the
os.FileInfoinformation provided by
os.Statis extremely helpful--and even includes the
os.FileModedata--providing it requires an additional system call for each node.
Because most callbacks only care about what the node type is, this library does not throw the type information away, but rather provides that information to the callback function in the form of a
os.FileModevalue. Note that the provided
os.FileModevalue that this library provides only has the node type information, and does not have the permission bits, sticky bits, or other information from the file's mode. If the callback does care about a particular node's entire
os.FileInfodata structure, the callback can easiy invoke
os.Statwhen needed, and only when needed.
$ 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
$ 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
filepath.Walk
I did not previously care about this either, but humor me. We all love how we can write once and run everywhere. It is essential for the language's adoption, growth, and success, that the software we create can run unmodified on all architectures and operating systems supported by Go.
When the traversed file system has a logical loop caused by symbolic links to directories, on unix
filepath.Walkignores symbolic links and traverses the entire directory tree without error. On Windows however,
filepath.Walkwill continue following directory symbolic links, even though it is not supposed to, eventually causing
filepath.Walkto terminate early and return an error when the pathname gets too long from concatenating endless loops of symbolic links onto the pathname. This error comes from Windows, passes through
filepath.Walk, and to the upstream client running
filepath.Walk.
The takeaway is that behavior is different based on which platform
filepath.Walkis running. While this is clearly not intentional, until it is fixed in the standard library, it presents a compatibility problem.
This library fixes the above problem such that it will never follow logical file sytem loops on either unix or Windows. Furthermore, it will only follow symbolic links when
FollowSymbolicLinksis set to true. Behavior on Windows and other operating systems is identical.
filepath.Walk
While this library strives to mimic the behavior of the incredibly well-written
filepath.Walkstandard library, there are places where it deviates a bit in order to provide a more easy or intuitive caller interface.
Since this library does not invoke
os.Staton every file system node it encounters, there is no possible error event for the callback function to filter on. The third argument in the
filepath.WalkFuncfunction signature to pass the error from
os.Statto the callback function is no longer necessary, and thus eliminated from signature of the callback function from this library.
Furthermore, this slight interface difference between
filepath.WalkFuncand this library's
WalkFunceliminates the boilerplate code that callback handlers must write when they use
filepath.Walk. Rather than every callback function needing to check the error value passed into it and branch accordingly, users of this library do not even have an error value to check immediately upon entry into the callback function. This is an improvement both in runtime performance and code clarity.
On every OS platform
filepath.Walkinvokes the callback function with a solidus (
/) delimited pathname. By contrast this library invokes the callback with the os-specific pathname separator, obviating a call to
filepath.Cleanin the callback function for each node prior to actually using the provided pathname.
In other words, even on Windows,
filepath.Walkwill invoke the callback with
some/path/to/foo.txt, requiring well written clients to perform pathname normalization for every file prior to working with the specified file. This is a hidden boilerplate requirement to create truly os agnostic callback functions. In truth, many clients developed on unix and not tested on Windows neglect this subtlety, and will result in software bugs when someone tries to run that software on Windows.
This library invokes the callback function with
some\path\to\foo.txtfor the same file when running on Windows, eliminating the need to normalize the pathname by the client, and lessen the likelyhood that a client will work on unix but not on Windows.
This enhancement eliminates necessity for some more boilerplate code in callback functions while improving the runtime performance of this library.
godirwalk.SkipThisis more intuitive to use than
filepath.SkipDir
One arguably confusing aspect of the
filepath.WalkFuncinterface that this library must emulate is how a caller tells the
Walkfunction to skip file system entries. With both
filepath.Walkand this library's
Walk, when a callback function wants to skip a directory and not descend into its children, it returns
filepath.SkipDir. If the callback function returns
filepath.SkipDirfor a non-directory,
filepath.Walkand this library will stop processing any more entries in the current directory. This is not necessarily what most developers want or expect. If you want to simply skip a particular non-directory entry but continue processing entries in the directory, the callback function must return nil.
The implications of this interface design is when you want to walk a file system hierarchy and skip an entry, you have to return a different value based on what type of file system entry that node is. To skip an entry, if the entry is a directory, you must return
filepath.SkipDir, and if entry is not a directory, you must return
nil. This is an unfortunate hurdle I have observed many developers struggling with, simply because it is not an intuitive interface.
Here is an example callback function that adheres to
filepath.WalkFuncinterface to have it skip any file system entry whose full pathname includes a particular substring,
optSkip. Note that this library still supports identical behavior of
filepath.Walkwhen the callback function returns
filepath.SkipDir.
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 }
This library attempts to eliminate some of that logic boilerplate required in callback functions by providing a new token error value,
SkipThis, which a callback function may return to skip the current file system entry regardless of what type of entry it is. If the current entry is a directory, its children will not be enumerated, exactly as if the callback had returned
filepath.SkipDir. If the current entry is a non-directory, the next file system entry in the current directory will be enumerated, exactly as if the callback returned
nil. The following example callback function has identical behavior as the previous, but has less boilerplate, and admittedly logic that I find more simple to follow.
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
The default behavior of this library is to ignore symbolic links to directories when walking a directory tree, just like
filepath.Walkdoes. However, it does invoke the callback function with each node it finds, including symbolic links. If a particular use case exists to follow symbolic links when traversing a directory tree, this library can be invoked in manner to do so, by setting the
FollowSymbolicLinksconfig parameter to
true.
The default behavior of this library is to always sort the immediate descendants of a directory prior to visiting each node, just like
filepath.Walkdoes. This is usually the desired behavior. However, this does come at slight performance and memory penalties required to sort the names when a directory node has many entries. Additionally if caller specifies
Unsortedenumeration in the configuration parameter, reading directories is lazily performed as the caller consumes entries. If a particular use case exists that does not require sorting the directory's immediate descendants prior to visiting its nodes, this library will skip the sorting step when the
Unsortedparameter is set to
true.
Here's an interesting read of the potential hazzards of traversing a file system hierarchy in a non-deterministic order. If you know the problem you are solving is not affected by the order files are visited, then I encourage you to use
Unsorted. Otherwise skip setting this option.
Researchers find bug in Python script may have affected hundreds of studies
This library provides upstream code with the ability to specify a callback function to be invoked for each directory after its children are processed. This has been used to recursively delete empty directories after traversing the file system in a more efficient manner. See the
examples/clean-emptiesdirectory for an example of this usage.
This library provides upstream code with the ability to specify a callback to be invoked for errors that the operating system returns, allowing the upstream code to determine the next course of action to take, whether to halt walking the hierarchy, as it would do were no error callback provided, or skip the node that caused the error. See the
examples/walk-fastdirectory for an example of this usage.