JavaRush/Courses/Go SELF/File system traversal error strategy

File system traversal error strategy

41 level, 4 lecture
Open

1. Errors during traversal: why this is normal

When you start working with a file system, you want to believe a fairy tale: “I’ll provide a path—and I’ll get a list of files back.” Reality is closer to a TV series: today the directory exists, tomorrow it’s deleted; a file had permissions, then they were revoked; in the middle of a walk a flash drive gets unplugged (yes, it happens). So errors are not rare—they’re part of the contract: in Go, errors are values we handle programmatically, not “an exception from reality.”

This is especially important to understand in traversal: WalkDir can visit thousands of paths, and the “ideal” error-handling strategy depends on why you’re walking the tree in the first place.

Where errors appear in WalkDir and what err means

filepath.WalkDir(root, fn) is built so that the library calls your function fn many times: once per path it encounters. That’s the callback: you don’t control the loop directly, but you control the reaction to each “encounter” with a file or directory.

Key point: the WalkDir handler receives three parameters, and one of them is err. And this err is not “something scary,” but a signal: “for the current path I (the walker) ran into a problem.” You don’t need to guess—you need to choose behavior: stop the entire walk, skip a subtree, or collect the error and keep going.

Reminder of the signature:

func(path string, d os.DirEntry, err error) error

And this is where the power shows up: the return value is the lever that controls traversal.

Mini model: three strategies

If we simplify hard, we have only three straightforward strategies:

  1. Stop on the first error: “without a complete result, there’s no point.”
  2. Skip the problematic spot: “a partial result is OK.”
  3. Collect all errors and continue: “I want maximum data + a report of issues.”

In Go this is especially natural, because errors are values, and we can store them, wrap them, classify them, and decide what to do next based on them.

2. Training app fsreport: a base for examples

So the examples are not just for show, imagine we’re writing a utility called fsreport: it recursively walks a directory and collects a list of files (for example, only .log), and then prints a short report.

Today we’ll add to fsreport what this lecture is about: a controllable traversal error strategy.

Mini data model (as a base; we’ll expand it later):

package main

type FileItem struct {
	Path string
	Size int64
}

Now let’s make an enum for the error strategy:

package main

type ErrorStrategy int

const (
	StopOnError ErrorStrategy = iota
	SkipPermissionErrors
	CollectErrors
)

3. Three error-handling strategies in practice

Strategy #1: stop on the first error

This strategy feels the most “strict,” but in practice it’s the simplest and often the most honest. If your result must be complete (for example, you’re building an index and it must include everything), then any error makes the result questionable. In that situation it’s logical to stop the walk immediately and bubble the error up.

The main technique here is to add context: don’t just return the error, wrap it so it’s clear which path we failed on. In Go, this is typically done with fmt.Errorf(... %w ...), to preserve the “cause” and enable checking it later via errors.Is.

Example function ScanStopOnError (short and to the point):

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func ScanStopOnError(root string) ([]FileItem, error) {
	var items []FileItem

	err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return fmt.Errorf("walk %q: %w", path, err)
		}
		if d.IsDir() {
			return nil
		}
		info, err := d.Info()
		if err != nil {
			return fmt.Errorf("info %q: %w", path, err)
		}
		items = append(items, FileItem{Path: path, Size: info.Size()})
		return nil
	})

	if err != nil {
		return nil, err
	}
	return items, nil
}

Notice two “sources of trouble”: err from WalkDir and a separate error from d.Info(). This is normal: the tree is “alive,” and you might not manage to read metadata even if the entry was just listed a moment ago.

Strategy #2: skip problematic spots

The “skip” strategy is when you say: “it’s more important for me to go as far as possible than to die on the first locked cabinet.” A typical example: you scan a home directory and don’t want to crash just because System Volume Information (or an equivalent) is inaccessible.

WalkDir has special “signal values” that look like errors but aren’t errors in meaning. The most important one for us is filepath.SkipDir: “don’t enter this directory, but keep walking the rest of the tree.”

A very common and useful variant: skip permission errors. For this we can check os.IsPermission(err).

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func ScanSkipPermissions(root string) ([]FileItem, error) {
	var items []FileItem

	err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			if os.IsPermission(err) {
				return filepath.SkipDir
			}
			return fmt.Errorf("walk %q: %w", path, err)
		}

		if d.IsDir() {
			return nil
		}
		info, err := d.Info()
		if err != nil {
			if os.IsPermission(err) {
				return nil // for a file: just skip it
			}
			return fmt.Errorf("info %q: %w", path, err)
		}

		items = append(items, FileItem{Path: path, Size: info.Size()})
		return nil
	})

	if err != nil {
		return nil, err
	}
	return items, nil
}

A subtle point: returning SkipDir makes sense specifically when “the problem is with a directory,” because the meaning is “don’t go inside.” For a file, “don’t go inside” is meaningless, so for a file it’s simpler to return nil and just not add it to the results.

Strategy #3: aggregate errors and continue

Sometimes the business requirement sounds like: “collect everything you can, but don’t hide problems.” For example, you’re building a log report, and it’s useful to know which files failed to yield metadata. With this strategy you don’t fail immediately, but you also don’t pretend everything is fine.

A practical pattern: during the walk, we append errors into []error, and at the end we return a single error via errors.Join. This is convenient because the calling code gets one error, but it contains all the causes.

Let’s write the function:

package main

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
)

func ScanCollectErrors(root string) ([]FileItem, error) {
	var items []FileItem
	var errs []error

	_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			errs = append(errs, fmt.Errorf("walk %q: %w", path, err))
			return nil
		}

		if d.IsDir() {
			return nil
		}
		info, err := d.Info()
		if err != nil {
			errs = append(errs, fmt.Errorf("info %q: %w", path, err))
			return nil
		}

		items = append(items, FileItem{Path: path, Size: info.Size()})
		return nil
	})

	if len(errs) > 0 {
		return items, errors.Join(errs...)
	}
	return items, nil
}

Why is it important to use %w when wrapping? Because it preserves the original cause, and later (in calling code) you can do errors.Is(err, os.ErrPermission) or some other check along the chain. In Go 1.13+ this is supported directly by the standard library via the idea of a “cause chain” (wrapping/unwrapping) and the functions errors.Is / errors.As.

4. How to choose a strategy and how to assemble everything into one function

Strategy selection table

Choosing a strategy is not about personal taste—it’s the answer to: “what matters more—completeness, fast feedback, or maximum usefulness of the result.” So it doesn’t turn into kitchen philosophy, here’s a compact table.

Strategy When it fits Pros Cons
StopOnError
If without a complete result there’s no point continuing Simple code, fail fast, easy to test You might not reach useful data because of a single problem
SkipPermissionErrors
If part of the tree is non-critical and you expect access issues Partial result, less noise, faster to “get to the goal” You can silently skip something important if the policy is too soft
CollectErrors
If you need maximum data + a report of all issues The most informative mode, great for reports and diagnostics Harder UX: you must be able to display/log an aggregated error

Universal function Scan

When you start writing real code, you don’t want three different functions—you want one with a parameter. That’s what we’ll do now: Scan(root, strategy).

package main

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
)

func Scan(root string, strategy ErrorStrategy) ([]FileItem, error) {
	var items []FileItem
	var errs []error

	walkErr := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			switch strategy {
			case StopOnError:
				return fmt.Errorf("walk %q: %w", path, err)
			case SkipPermissionErrors:
				if os.IsPermission(err) {
					return filepath.SkipDir
				}
				return fmt.Errorf("walk %q: %w", path, err)
			case CollectErrors:
				errs = append(errs, fmt.Errorf("walk %q: %w", path, err))
				return nil
			}
		}

		if d.IsDir() {
			return nil
		}

		info, infoErr := d.Info()
		if infoErr != nil {
			if strategy == SkipPermissionErrors && os.IsPermission(infoErr) {
				return nil
			}
			if strategy == CollectErrors {
				errs = append(errs, fmt.Errorf("info %q: %w", path, infoErr))
				return nil
			}
			return fmt.Errorf("info %q: %w", path, infoErr)
		}

		items = append(items, FileItem{Path: path, Size: info.Size()})
		return nil
	})

	if walkErr != nil {
		return nil, walkErr
	}
	if len(errs) > 0 {
		return items, errors.Join(errs...)
	}
	return items, nil
}

Yes, it’s a bit longer than “one strategy,” but now the behavior is explicit and controllable. And also testable: you can run Scan in different modes and compare the result.

Flowchart: how to think inside the WalkDir callback

Sometimes it helps to literally “see” what we’re doing to stop getting confused by return nil, return err, SkipDir, and “where am I even returning to?”

flowchart TD
    A["WalkDir calls the handler(path, d, err)"] --> B{err != nil?}
    B -- "yes" --> C{Strategy?}
    C -- "StopOnError" --> D["return wrap(err)"]
    C -- "SkipPermissionErrors" --> E{"os.IsPermission(err)?"}
    E -- "yes" --> F["return filepath.SkipDir"]
    E -- "no" --> D
    C -- "CollectErrors" --> G["append(errs, wrap(err)); return nil"]
    B -- "no" --> H{"d.IsDir()?"}
    H -- "yes" --> I["return nil"]
    H -- "no" --> J["info, infoErr := d.Info()"]
    J --> K{infoErr != nil?}
    K -- "yes" --> L["react based on the strategy"]
    K -- "no" --> M["append(items, FileItem{...}); return nil"]

5. Errors with context: why you shouldn’t return “just err”

As a beginner, you really want to write: return err—and that’s it. But almost immediately you run into: “there is an error, but where exactly did it happen?” That’s why we add context: the operation and the path.

At the same time, it’s important not to lose the original cause if you want to distinguish later between “no permission” and “missing file.” To preserve the cause, use %w in fmt.Errorf. This is Go’s standard mechanism: an error can “contain” another error, forming a chain of causes, and errors.Is and errors.As can walk that chain.

If you remember one practical formula: “an error without a path is almost always half an error.”

6. Mini example main: run and compare modes

To feel that the strategy really changes program behavior, let’s write the simplest possible main. No flags or arguments here (we haven’t discussed them yet); we just scan the current directory ".".

package main

import (
	"fmt"
)

func main() {
	items, err := Scan(".", CollectErrors)
	fmt.Println("files:", len(items)) // files: 123 (example)

	if err != nil {
		fmt.Println("scan errors:", err)
	}
}

In

StopOnError
mode you may see that the program would stop immediately at the first problematic spot. In
SkipPermissionErrors
mode it will most likely “make it” further, but it will silently skip inaccessible parts. In
CollectErrors
mode it will show both the file count and a combined “wall of text” of issues—honestly.

7. Typical mistakes

Mistake #1: ignore err parameter in the WalkDir handler.
This is the most insidious slip: you start using d.Name() or d.IsDir(), even though the contract already told you “there was an error on this path.” In that situation d may not be something you can trust. The habit must be ironclad: the first check inside the callback is if err != nil { ... }.

Mistake #2: return a regular error when you meant to just “skip.”
When you return fmt.Errorf(...), you tell WalkDir: “stop.” When you return filepath.SkipDir, you tell it: “don’t go into this directory, but continue.” If you mix up these meanings, you get weird effects: sometimes traversal stops unexpectedly, sometimes you don’t understand why whole branches of the tree “disappeared.”

Mistake #3: use filepath.SkipDir for a file and expect magic.
SkipDir only makes sense for a directory: it literally means “don’t go inside.” If you return it for a file, you’ll get behavior that’s hard to explain to yourself a week later. For a file, “skip” usually means return nil and “don’t add it to the results.”

Mistake #4: print an error without path context.
A message like “permission denied” without a path is like “something broke” without specifying what. Always include at least the path and the operation: walk, info, stat. And to preserve the cause so it can be classified, wrap with %w.

Mistake #5: aggregate errors but forget to return the final error to the caller.
Sometimes code neatly appends errors into errs, and then returns (items, nil)—and you get “we did everything perfectly,” even though half the tree wasn’t readable. If you chose the aggregation strategy, it must end with an explicit result: either errors.Join(errs...), or at least printing/logging at the application boundary.

1
Task
Go SELF,  41 level,  4 lecture
Locked
Strict scan
Strict scan
1
Task
Go SELF,  41 level,  4 lecture
Locked
Gentle scan
Gentle scan
1
Task
Go SELF,  41 level,  4 lecture
Locked
Size collection
Size collection
1
Task
Go SELF,  41 level,  4 lecture
Locked
Universal scan
Universal scan
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet