AlgoMaster Logo

filepath Package

Medium Priority18 min readUpdated June 6, 2026

A path that works on a Linux dev machine can break the moment the same code runs on a Windows build server. The slash direction changes, the volume prefix appears, and string concatenation that "looks fine" produces invalid paths. The path/filepath package solves this by joining, splitting, cleaning, and walking paths with the rules of the host operating system, so your code stays portable without sprinkling runtime.GOOS checks everywhere.

Why path/filepath Exists Separately From path

Go ships two path packages, and the difference between them is easy to miss. The path package always uses forward slashes. It's for paths that are not tied to an operating system: URL paths, slash-separated identifiers in a manifest, keys in an object store. The path/filepath package uses the host OS separator. On Linux and macOS that's /, on Windows that's \. It's for paths on the local filesystem.

If you join "assets" and "products" with path.Join, you always get assets/products. If you join them with filepath.Join on Windows, you get assets\products. Mix the two up and you'll either confuse the operating system or write code that quietly stops working when ported.

PackageSeparatorUse When
pathAlways /URL paths, slash-separated keys, anything not on the local filesystem
path/filepathOS-specific (/ on Unix, \ on Windows)Real files and directories on disk

There's a small group of functions with the same names in both packages (Join, Split, Base, Dir, Ext, Clean, IsAbs). The signatures look identical but the behavior differs in the separator they emit. Importing the wrong one rarely produces a compile error and almost always produces a portability bug.

On Windows, the second line would print assets\products\2024\widgets. The first line stays the same because URLs do not care about the host OS.

Separators and Path Lists

The filepath package exposes three constants that name the separator characters used by the host OS.

ConstantTypeUnixWindowsMeaning
filepath.Separatorrune'/''\\'Separator between path elements
filepath.PathSeparatorrune'/''\\'Same as Separator, kept for symmetry
filepath.ListSeparatorrune':'';'Separator in path lists (e.g., PATH env var)

Most code never reads these directly because Join, Split, and friends already use them. The one place they show up is parsing environment variables that hold lists of paths.

The same code on Windows would split on ; because that's the convention for the PATH and PATHEXT variables there. Using the constant means the program reads correctly under either shell.

Joining and Splitting Paths

filepath.Join is the workhorse. It takes any number of strings, joins them with the OS separator, and runs the result through filepath.Clean. Cleaning removes empty segments, collapses multiple separators, and resolves . and .. where it can.

Three rules apply. Stray slashes inside arguments do not produce double slashes. Empty arguments are silently dropped. The result has no trailing separator unless the last argument was just a separator. Hand-rolling this with + or fmt.Sprintf("%s/%s", a, b) runs into all three traps.

filepath.Split does the opposite, sort of. It returns the directory part (including the trailing separator) and the file part. Together they reproduce the original string.

The directory includes the trailing slash, which is intentional: it lets dir + file == original even when one part is empty. If you don't need that property, filepath.Dir and filepath.Base give cleaner results.

FunctionInputOutput (Unix)Notes
Join("a", "b", "c.txt")three partsa/b/c.txtCleans the result
Split("a/b/c.txt")full path"a/b/", "c.txt"Trailing slash on dir
Dir("a/b/c.txt")full patha/bNo trailing slash
Base("a/b/c.txt")full pathc.txtLast element
Ext("c.txt")filename.txtIncludes the dot
Clean("./a//b/../c")messy patha/cCanonicalizes

Base, Dir, and Ext are the standard helpers when you want one piece of a path. They handle edge cases that string-slicing would miss.

Ext returns everything from the last dot in the filename, including the dot. A file named archive.tar.gz reports .gz, not .tar.gz. That matches Unix conventions, and it's almost always what you want for type detection.

Cleaning and Canonicalizing Paths

filepath.Clean is the function that makes the rest of the package behave. It runs a small algorithm that:

  1. Replaces multiple separators with a single one.
  2. Eliminates each . element.
  3. Eliminates each .. element along with the non-.. element before it.
  4. Eliminates .. elements that begin a rooted path: "/.." becomes "/".

The result is the shortest equivalent path string, by purely lexical analysis. Clean never touches the filesystem. It does not resolve symbolic links and does not check that any of the path elements exist.

Two outputs deserve attention. The empty string becomes ".", because Clean guarantees a non-empty result. And assets/../.. becomes ".." rather than collapsing to ".", because the relative path really does escape the current directory.

The third example is the one to remember. "/var/www/../etc/passwd" is a perfectly valid path on Unix that, after cleaning, points to /etc/passwd. Cleaning does not stop traversal, it just normalizes the form. We'll come back to this in the security section.

Absolute and Relative Paths

A path is absolute when it starts from a root the operating system recognizes (a leading / on Unix, a drive letter or UNC share on Windows). Otherwise it's relative, interpreted from the process's current working directory. filepath has three functions to bridge between the two.

filepath.Abs calls os.Getwd internally if the input is relative, joins the result, and cleans it. That's why it returns an error: Getwd can fail (the directory might have been deleted while the process is running).

filepath.Rel answers "what relative path from base reaches target?" If the target is not inside the base, it returns a path that uses .. to climb out. If the two paths are on different volumes (Windows), it returns an error.

A common e-commerce use is showing a download URL relative to a public directory:

filepath.ToSlash converts OS separators to forward slashes, which is the right thing to do when feeding the result into a URL. There's a matching filepath.FromSlash that goes the other direction.

Matching and Globbing

filepath.Match and filepath.Glob use shell-style wildcards, not regular expressions. The pattern syntax is small.

TokenMatches
*Any sequence of non-separator characters
?Any single non-separator character
[abc]One of the characters in the set
[a-z]One character in the range
[^abc]One character not in the set

Match tests a single name against a pattern and returns a boolean. Glob walks the filesystem and returns every existing path that matches.

The * wildcard does not cross directory separators. *.csv matches report.csv but not 2024/report.csv. For a recursive search you walk the tree yourself, which is what the next section covers.

Glob does touch the filesystem. It returns the list of paths that exist and match the pattern. The order of results is lexical.

Glob returns an error only when the pattern itself is malformed (for example, an unclosed [). A pattern that doesn't match anything is not an error; you get an empty slice and a nil error.

Walking a Directory Tree

filepath has two functions for visiting every entry in a tree.

FunctionSinceVisit Callback SignatureWhy It Exists
WalkGo 1.0func(path string, info fs.FileInfo, err error) errorOriginal API
WalkDirGo 1.16func(path string, d fs.DirEntry, err error) errorFaster, avoids per-file Stat

Walk calls os.Lstat on every entry before invoking the callback, which produces a full fs.FileInfo (size, mode, mod time). For a large tree, that's one syscall per file even if the callback only cares about the name.

WalkDir passes a fs.DirEntry instead. The entry already knows the name and whether it's a directory (information the kernel returned with the parent's readdir call), so no extra Stat is needed. If the callback actually wants the full FileInfo, it calls d.Info() on demand. On a tree with 100k files where the callback only filters by extension, WalkDir can be several times faster.

Use `WalkDir` in new code. Use Walk only when reading old code or when you genuinely need FileInfo for every entry.

Here's a walker that lists every CSV report under an orders/ tree, the kind of thing a daily batch job does to pick up new exports:

A few rules apply to the callback. The first argument is the full path, joined with the OS separator. If the walk hits a permission error while reading a directory, the callback receives the error in the third argument, and the entry may be partial. Returning a non-nil error from the callback stops the walk and propagates the error out of WalkDir. Returning nil continues.

The structure of a WalkDir traversal looks like this:

The walk visits each node depth-first in lexical order. The pruned branch on the left shows what SkipDir does: when the callback returns it for a directory, the walker stops descending into that directory and moves to the next sibling.

Pruning With SkipDir and SkipAll

Two sentinel errors steer the walk without stopping it:

SentinelSinceEffect
filepath.SkipDirGo 1.0Skip the current directory's contents (or the current file's siblings if returned from a non-directory)
filepath.SkipAllGo 1.20Stop the entire walk cleanly, with no error returned from WalkDir

SkipDir is what you want for "don't recurse into this folder." A common case is skipping a vendored archive of old reports:

SkipAll is useful when you're searching for a single match and want to stop the moment you find it. Before Go 1.20 you had to return a custom sentinel and unwrap it at the call site, which is uglier than it sounds:

WalkDir swallows SkipAll internally, so the err check is defensive. The walk stops the instant the target appears, instead of visiting the rest of the tree.

Volumes and Symbolic Links

Two helpers come up less often but are worth knowing.

filepath.VolumeName returns the leading volume part of a path. On Unix it always returns the empty string. On Windows it returns the drive letter or UNC share. Cross-platform code that needs to strip the volume to compute a relative web path can use it without a runtime.GOOS switch.

The last two lines return non-empty results when the program runs on Windows ("C:" and "\\fileserver\share" respectively). On Unix, the same strings are just regular paths with backslashes in their names.

filepath.EvalSymlinks resolves every symbolic link in the path and returns the real, canonical location on disk. It touches the filesystem (every component is checked), so it's slower than Clean and can fail if any element is missing.

If /var/log/syslog is a symlink that points to a rotated file, EvalSymlinks returns the destination. This matters for security checks where you want to know what file the path really refers to, not what it appears to refer to.

Path Traversal: Why Clean Is Not Enough

A common pattern in e-commerce code is a download endpoint that serves a file from a fixed directory. The handler reads a filename from the request and joins it with the root:

The second call is the bug. filepath.Clean("../../etc/passwd") returns "../../etc/passwd" because the input is a relative path that legitimately climbs the tree. filepath.Join then resolves the .. against the root, and you end up serving /var/etc/passwd. This is the classic path traversal vulnerability. Cleaning the input did not stop it; cleaning is a syntactic operation, and .. is a syntactically valid path element.

The fix has two parts. First, compute the absolute path of both the root and the candidate. Second, verify that the candidate is actually inside the root.

The trailing separator in absRoot is important. Without it, HasPrefix would accept /var/www/store/downloads-secret as "inside" /var/www/store/downloads. With it, the check is for a true subdirectory.

A second technique uses filepath.Rel. If the relative path from the root to the candidate starts with .., the candidate is outside.

Both techniques are valid. The HasPrefix form is slightly faster for many requests because it avoids the Rel computation. The Rel form is harder to get wrong because it doesn't depend on the trailing-separator trick.

There's one more class of attacks to consider: symbolic links inside the download directory that point outside it. Neither check catches a /var/www/store/downloads/escape -> /etc/passwd symlink. To defend against that, call filepath.EvalSymlinks on the candidate before the prefix check. The cost is real (it stats every component), so a typical pattern is to allow symlinks only at upload time and reject them when serving.

Quiz

filepath Package Quiz

10 quizzes