What do fs.SkipDir and fs.SkipAll do when returned from a filepath.WalkDir callback?
answer
- two sentinels that do not mean failure
- one prunes, one stops everything
- the walk still returns nil
- on a file it means the parent
- guard the check with IsDir
basics
~10 sReturning fs.SkipDir skips the current directory's contents, or the rest of the containing directory when the entry is a file. fs.SkipAll ends the whole walk immediately, and filepath.WalkDir still returns nil.
solid answer
~50 sBoth are sentinel errors that the walk interprets rather than propagates. Returning `fs.SkipDir` while visiting a **directory** means "do not descend into this one" — the callback already ran for the directory itself, and its children are never visited. Returning it while visiting a **file** is the part people get wrong: it skips the remaining entries of that file's *parent* directory, not just that one file. Returning `fs.SkipAll` abandons the entire traversal at once. In both cases `filepath.WalkDir` returns `nil` to its caller, because neither sentinel means failure — only a non-sentinel error stops the walk *and* comes back as an error. In an indexer that walks a checked-out repository, `fs.SkipDir` on directories such as `.git` or `vendor` is what keeps most of the tree from ever being read, and `fs.SkipAll` is how you stop the moment you have found what you came for.
code
go · 13 lineserr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && (d.Name() == ".git" || d.Name() == "vendor") {
return fs.SkipDir // its contents are never read
}
if d.Name() == "MANIFEST" {
found = path
return fs.SkipAll // WalkDir returns nil, not an error
}
return nil
})go deeper
Remember that a walk callback controls the traversal through its return value, and that returning fs.SkipDir on a directory is how you avoid descending into it at all.
Be precise about the file case and about the return value: fs.SkipDir on a non-directory skips the rest of the containing directory, and neither sentinel reaches the caller as an error.
Show you have debugged the failure mode: a name check that matched a file rather than a directory, dropping whatever sorted after it, reproducible only for a particular directory ordering.
Think about the interface you expose. If your scanner takes a caller-supplied filter, decide whether callers can prune subtrees at all, since that choice sets the cost ceiling of every scan built on it.
## Three ways a callback can end the traversal The return value of an `fs.WalkDirFunc` is the only control channel a walk gives you, so it is overloaded with three meanings: 1. **`nil`** — carry on normally. If the current entry is a directory, descend into it. 2. **`fs.SkipDir`** — prune. What gets pruned depends on the current entry (below). 3. **`fs.SkipAll`** — stop the entire walk, successfully. 4. Anything else — stop the entire walk and return that error to the caller of `filepath.WalkDir`. The first thing to internalise is that `fs.SkipDir` and `fs.SkipAll` are of type `error` but do not mean failure. `filepath.WalkDir` recognises them by identity, acts on them, and returns `nil`. Your caller cannot tell from the return value whether a walk finished naturally or was cut short by `fs.SkipAll`; if that distinction matters, record it in a variable your callback closes over. ## fs.SkipDir depends on what you are visiting **On a directory** the meaning is the obvious one: prune this subtree. The callback has already been invoked for the directory entry itself — that is how you got the chance to inspect its name — and returning `fs.SkipDir` means its contents are never read. The walk then continues with the next sibling. This is the pruning primitive: on a deep tree of very many small entries, most of which you do not care about, pruning at the directory level is worth far more than filtering per file, because a pruned directory is never even read. **On a file** the meaning is much less obvious: the walk skips the remaining entries in the file's containing directory. It is not "skip this one file" — that is what returning `nil` after ignoring the entry does. The rule reads more sensibly if you phrase it as "skip the directory this path belongs to": for a directory entry, the directory *is* the path; for a file, it is the parent. Confusing the two produces a walk that silently drops entries, and because the entries dropped are whatever happened to sort after the trigger file, the loss is order-dependent and maddening to reproduce. ## fs.SkipAll `fs.SkipAll` means "I am done". No further entries are visited anywhere, and the walk returns `nil`. It is the clean way to implement "find the first match and stop", which matters when the alternative is walking the rest of a very large tree for nothing. Before this sentinel existed, the idiom was to return a private sentinel of your own and unwrap it at the top: ```go var errFound = errors.New("found") err := filepath.WalkDir(root, fn) if errors.Is(err, errFound) { err = nil } ``` That still works and is still the pattern when you want to carry information out with the stop signal, but for a plain early exit `fs.SkipAll` says it directly. ## Where the names live Both sentinels are declared in `io/fs`, so they read as `fs.SkipDir` and `fs.SkipAll`. `path/filepath` also exposes `filepath.SkipDir`, which predates `io/fs` and is now the same value — comparisons and `errors.Is` work across the two names. New code usually writes the `fs.` form because the callback type itself, `fs.WalkDirFunc`, comes from `io/fs`. ## Compare by identity, not by message If you wrap errors on the way out of a callback — `fmt.Errorf("indexing %s: %w", path, err)` — be careful not to wrap the sentinels. A wrapped `fs.SkipDir` is no longer the value the walk compares against, so instead of pruning a subtree you will abort the whole traversal with an error. Return the sentinels bare. ## A pruning callback in practice ```go err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() && (d.Name() == ".git" || d.Name() == "vendor") { return fs.SkipDir } return record(path, d) }) ``` One subtlety: the check must be guarded by `d.IsDir()`. A *file* called `vendor` would otherwise return `fs.SkipDir` and silently truncate its parent directory's listing — which is the file-case rule biting exactly where nobody looks for it.
- A callback returns fs.SkipDir while visiting a regular file. What exactly gets skipped?The remaining entries of that file's containing directory. It is not a per-file skip: everything that sorts after the trigger file in that directory, including subdirectories, is never visited. To ignore a single file you simply return nil without recording it. This is why a name check that can match a file should be guarded by d.IsDir() before returning fs.SkipDir.
- If a callback returns fs.SkipAll, what does filepath.WalkDir return to its caller?nil. Both sentinels are recognised by the walk and never propagate, so a caller cannot distinguish a completed walk from one cut short. If you need to know, set a flag in the closure at the same moment you return fs.SkipAll, and inspect it after the walk returns.
- How would you stop a walk early and carry a reason out with the stop signal?Return your own sentinel created with errors.New, then filter it at the top with errors.Is and treat it as success. That predates fs.SkipAll and is still the pattern when the stop carries information, since a wrapped error can hold context. Just never wrap fs.SkipDir or fs.SkipAll themselves, because the walk compares them by identity.
fs.SkipDir is closing one folder in a filing cabinet and moving to the next; fs.SkipAll is shutting the whole cabinet and walking away satisfied.
saying these in an interview costs you the question
- Thinks fs.SkipDir on a file skips only that file
- Expects filepath.WalkDir to return fs.SkipAll as an error
- Wraps fs.SkipDir with fmt.Errorf and %w before returning it
- Returns fs.SkipDir without first checking d.IsDir()
- Believes the callback is not called for a directory it prunes