Do filepath.Clean and filepath.Rel touch the filesystem, and how is filepath.Abs different?
answer
- string in, string out, mostly
- no system call for the common ones
- one of them asks the process a question
- os.Getwd is hiding inside exactly one
basics
~20 sNo. Clean, Rel, Join, Base, Dir and Ext are pure string operations that never open a file or resolve a symbolic link. filepath.Abs is the exception: for a relative input it prepends the process's working directory.
solid answer
~50 sAlmost everything in `path/filepath` is lexical. `Clean`, `Join`, `Split`, `Base`, `Dir`, `Ext`, `Rel`, `ToSlash` and `VolumeName` rewrite a string and never consult the filesystem — which is why `filepath.Clean("a/b/../c")` is `a/c` even when `b` is a symbolic link pointing somewhere else, and the cleaned name can therefore denote a different file than the original would have. `filepath.EvalSymlinks` is the function that asks the filesystem for the real answer. `filepath.Abs` is the odd one out in a second way: if its argument is not already absolute it joins the process's current working directory, obtained with `os.Getwd`, and returns an error if that fails — so its result depends on process-wide state that any goroutine can change with `os.Chdir`. `filepath.Rel` stays lexical and returns an error rather than reaching for the working directory when the two names cannot be related.
code
go · 10 linesfunc show() {
fmt.Println(filepath.Clean("a/b/../c")) // a/c - b is never opened
rel, _ := filepath.Rel("/srv/app", "/srv/app/conf/x.yaml")
fmt.Println(rel) // conf/x.yaml
_ = os.Chdir("/srv/app")
abs, _ := filepath.Abs("conf/x.yaml")
fmt.Println(abs) // /srv/app/conf/x.yaml - via os.Getwd
}go deeper
Remember that most path/filepath functions only rewrite a string and never check that the file exists. filepath.Abs is the one that needs the process's current directory to do its job.
Explain the split precisely: Clean, Join, Base, Dir, Ext and Rel are lexical, Abs consults os.Getwd and can return an error, and EvalSymlinks is the one that walks the real filesystem.
Demonstrate the consequence: a lexically cleaned name can denote a different file once a symlink is involved, and a relative name resolved at request time is at the mercy of process-wide state that any goroutine can change.
Set the rule for the codebase — base directories resolved to absolute names once at startup, no runtime chdir, and an explicit decision about which layer is allowed to ask the filesystem anything at all.
## Three tiers, not one It is worth sorting `path/filepath` into three groups, because interview answers that miss the split usually go wrong in the same place. **Tier 1 — purely lexical.** `Clean`, `Join`, `Split`, `Base`, `Dir`, `Ext`, `Rel`, `Match`, `ToSlash`, `FromSlash`, `VolumeName`. These take strings and return strings. No system call, no `stat`, no open, no symlink resolution. They will happily manipulate the name of a file that does not exist and never has. **Tier 2 — depends on process state.** `Abs`. If the argument is already absolute, `Abs` just cleans it. If it is relative, `Abs` calls `os.Getwd` and joins the result. That makes it the only common `filepath` function whose output for a given input can change over the life of a process. **Tier 3 — consults the filesystem.** `EvalSymlinks`, which walks the name resolving every symbolic link and returns an error if a component does not exist, and `Glob`, which reads directories to expand a pattern. ## Why lexical cleaning can change meaning This is the part that separates a memorised answer from an understood one. Consider `a/b/../c` where `b` is a symbolic link to `/other/dir`. - **The operating system** resolves left to right: it enters `a`, follows `b` to `/other/dir`, then `..` takes it to `/other`, so the name means `/other/c`. - **`filepath.Clean`** does not know `b` is a link. It cancels `b` against `..` textually and returns `a/c`. Both are correct for what they are: `Clean` promises a *lexically* equivalent path, not an equivalent one on this machine. The consequence for real code is that cleaning a name is not the same as resolving it, and any logic that must know which file is really meant needs `filepath.EvalSymlinks` or, better, a handle to the file it already opened. ## `Abs`, `os.Getwd` and the process-wide working directory `filepath.Abs(path string) (string, error)` is documented to use the current working directory to turn a relative path into an absolute one, and to clean the result. Two things follow. First, **it can fail.** `os.Getwd` returns an error if, for example, the working directory has been removed underneath the process. Code that writes `abs, _ := filepath.Abs(p)` is discarding a real failure mode. Second, **the working directory is per process, not per goroutine.** Go has no per-goroutine current directory. A single call to `os.Chdir` anywhere in the program changes what `filepath.Abs` returns and what every relative `os.Open` resolves to, for every goroutine, immediately. In a server that is a race with no lock to take: a request handler that chdirs to scope its relative names has just repointed every other in-flight handler. The standard discipline is therefore: - resolve the base directory **once at startup** with `filepath.Abs`, check the error, and keep the absolute string; - build every later name with `filepath.Join` from that base; - never call `os.Chdir` in a long-running process. ## When `Rel` returns an error `filepath.Rel(basepath, targpath string) (string, error)` produces a relative name that is *lexically equivalent* to `targpath` when joined to `basepath`. It is entirely a string computation, and it reports an error rather than guessing when it cannot do the job: - one path is absolute and the other relative; - on Windows, the two names are on different volumes; - the answer would require knowing the current working directory — `Rel` refuses to consult it. That is a deliberate design: the lexical functions never quietly reach for process state. If you want the working directory involved, you make both names absolute yourself first, and you handle the error from doing so. ## How to say it in an interview "Most of `filepath` is string manipulation — it does not know whether the file exists. `Abs` is the one that consults the process working directory, and `EvalSymlinks` is the one that consults the filesystem. That distinction matters because a lexically cleaned path and the path the kernel would resolve are not the same thing once a symlink is involved, and because relying on the working directory at request time makes behaviour depend on global mutable state."
- Why can filepath.Clean change which file a name refers to?Because it cancels `..` lexically: `a/b/../c` becomes `a/c`. If `b` is a symbolic link to `/other/dir`, the kernel would have resolved `a/b/..` to `/other`, so the original and the cleaned name point at different places. `filepath.EvalSymlinks` is the call that gives you the filesystem's answer instead of the string's.
- When does filepath.Rel return an error instead of a relative path?When the two names cannot be related lexically: one absolute and one relative, different volumes on Windows, or a case where computing the answer would need the current working directory. Rel never falls back to `os.Getwd`; it reports the error, which is why callers usually make both names absolute themselves first and handle that error explicitly.
- Why is calling os.Chdir inside a running server a hazard?The working directory is process-wide, not per goroutine, so one call repoints `filepath.Abs` and every relative open for every other goroutine at once, with no lock that could serialise it. The usual rule is to compute an absolute base directory once at startup, join everything under it, and never chdir at runtime.
saying these in an interview costs you the question
- Thinks filepath.Clean resolves symbolic links
- Expects filepath.Abs to be a pure function
- Believes filepath.Rel falls back to the working directory
- Assumes Clean checks that the path exists
- Calls os.Chdir per request to scope relative names