Go filepath
last modified October 30, 2026
In this article we show how to work with file paths in Go. The
path/filepath package manipulates paths in the way the target
operating system expects, so the same program behaves correctly on Linux,
macOS and Windows.
All examples were compiled and run with Go 1.27.0 on Linux. The examples that
inspect files use the /tmp/gofp working directory and create their
own test tree there.
Joining and cleaning paths
filepath.Join joins any number of elements with the separator of
the operating system and cleans the result. Empty elements are ignored, and
.. removes the preceding element.
package main
import (
"fmt"
"path/filepath"
)
func main() {
fmt.Println(filepath.Join("a", "b", "..", "c"))
fmt.Println(filepath.Join("a", "", "b"))
fmt.Println(filepath.Clean("a//b/../c/./d"))
fmt.Println(filepath.Clean(""))
}
Clean works purely on the text of the path; it does not check
whether the path exists. The cleaned form of an empty path is
".", the current directory.
$ go run main.go a/c a/b a/c/d .
Base, Dir, Ext and Split
Base returns the last element of a path, Dir returns
everything before it, and Ext returns the suffix starting at the
last dot. Split returns both parts at once, keeping the trailing
separator in the directory.
package main
import (
"fmt"
"path/filepath"
)
func main() {
p := "/home/user/data/report.tar.gz"
fmt.Println("Base:", filepath.Base(p))
fmt.Println("Dir: ", filepath.Dir(p))
fmt.Println("Ext: ", filepath.Ext(p))
dir, file := filepath.Split(p)
fmt.Printf("Split: %q %q\n", dir, file)
fmt.Println("Base /:", filepath.Base("/"))
fmt.Println("Dir file:", filepath.Dir("main.go"))
fmt.Println("Ext Makefile:", filepath.Ext("Makefile"))
}
Only the last extension is returned, so report.tar.gz has the
extension .gz. A path without a dot, such as
Makefile, has an empty extension, and Dir of a bare
file name is the current directory.
$ go run main.go Base: report.tar.gz Dir: /home/user/data Ext: .gz Split: "/home/user/data/" "report.tar.gz" Base /: / Dir file: . Ext Makefile:
Absolute and relative paths
Abs resolves a relative path against the current working
directory, IsAbs reports whether a path starts at the root, and
Rel computes a relative path between two absolute paths.
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
wd, err := os.Getwd()
if err != nil {
fmt.Println(err)
return
}
abs, err := filepath.Abs("data/report.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println("cwd: ", wd)
fmt.Println("abs: ", abs)
fmt.Println("isabs:", filepath.IsAbs(abs), filepath.IsAbs("data/report.txt"))
rel, err := filepath.Rel(wd, abs)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("rel: ", rel)
}
The absolute path is built from the directory where the program runs, so the
first two lines differ on another machine. Rel reverses the
operation and returns the original relative path.
$ go run main.go cwd: /tmp/gofp abs: /tmp/gofp/data/report.txt isabs: true false rel: data/report.txt
Matching a single path
filepath.Match checks one name against a shell-style pattern. A
star matches any sequence of characters except the path separator, a question
mark matches one character, and brackets match a character class.
package main
import (
"fmt"
"path/filepath"
)
func main() {
cases := [][2]string{
{"*.go", "main.go"},
{"*.go", "main.txt"},
{"data/*.txt", "data/a.txt"},
{"data/*.txt", "data/sub/a.txt"},
{"[a-c]*", "banana"},
{"a[", "ab"},
}
for _, c := range cases {
ok, err := filepath.Match(c[0], c[1])
fmt.Printf("%-12s %-14s %v %v\n", c[0], c[1], ok, err)
}
}
The fourth case is false because * does not cross a separator;
matching several levels requires a different approach. The last pattern is
malformed, so Match returns ErrBadPattern together
with false.
$ go run main.go *.go main.go true <nil> *.go main.txt false <nil> data/*.txt data/a.txt true <nil> data/*.txt data/sub/a.txt false <nil> [a-c]* banana true <nil> a[ ab false syntax error in pattern
Glob patterns
filepath.Glob returns the existing paths that match a pattern.
Unlike a shell, Go does not treat ** as a recursive wildcard;
it is simply two stars, so it matches one path element.
package main
import (
"fmt"
"path/filepath"
)
func main() {
patterns := []string{"data/*.txt", "data/*/*.txt", "data/**/*.txt"}
for _, p := range patterns {
matches, err := filepath.Glob(p)
fmt.Printf("%-16s %v %v\n", p, matches, err)
}
}
The tree contains data/a.txt, data/b.md and
data/sub/c.txt. The double star finds the same file as a single
star, because both match exactly one directory level. A recursive search uses
WalkDir.
$ mkdir -p data/sub $ touch data/a.txt data/b.md data/sub/c.txt $ go run main.go data/*.txt [data/a.txt] <nil> data/*/*.txt [data/sub/c.txt] <nil> data/**/*.txt [data/sub/c.txt] <nil>
Walking a directory tree
filepath.WalkDir visits a directory and all its descendants in
depth-first order. The callback receives the path, a directory entry and any
error from the walk.
package main
import (
"fmt"
"io/fs"
"path/filepath"
)
func main() {
err := filepath.WalkDir("data", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
fmt.Println("dir ", path)
} else {
fmt.Println("file", path)
}
return nil
})
if err != nil {
fmt.Println(err)
}
}
The root itself is the first entry. Returning fs.SkipDir for a
directory would prune its subtree; returning any other error stops the walk and
is returned by WalkDir.
$ go run main.go dir data file data/a.txt file data/b.md dir data/sub file data/sub/c.txt
Separators, volumes and path lists
ToSlash and FromSlash convert the native separator to
and from a forward slash. VolumeName reports a drive letter on
Windows, and SplitList splits a list such as PATH
using the operating system separator.
package main
import (
"fmt"
"path/filepath"
)
func main() {
fmt.Println("ToSlash: ", filepath.ToSlash(`a\b\c`))
fmt.Println("FromSlash:", filepath.FromSlash("a/b/c"))
fmt.Println("Volume: ", filepath.VolumeName("/home/user"))
fmt.Println("SplitList:", filepath.SplitList("/usr/bin:/bin:/usr/local/bin"))
fmt.Printf("Separator: %q\n", filepath.ListSeparator)
}
On Linux the backslash is an ordinary character in a file name, so
ToSlash leaves it alone. On Windows the same call would convert it
to a forward slash. The volume name is empty on Unix, and the list separator is
a colon.
$ go run main.go ToSlash: a\b\c FromSlash: a/b/c Volume: SplitList: [/usr/bin /bin /usr/local/bin] Separator: ':'
Resolving symbolic links
filepath.EvalSymlinks follows symbolic links and returns the path
of the final target. The result is cleaned but is not necessarily absolute.
package main
import (
"fmt"
"path/filepath"
)
func main() {
target, err := filepath.EvalSymlinks("link.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(target)
abs, err := filepath.Abs(target)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(abs)
}
The link points to a file in the same directory, so the resolved path is
relative. Abs turns it into a full path when one is needed.
$ printf 'hello\n' > target.txt $ ln -s target.txt link.txt $ go run main.go target.txt /tmp/gofp/target.txt
Source
Go path/filepath package - reference
In this article we have worked with file paths in Go. We joined and cleaned paths, split them into directory, base and extension, converted between absolute and relative forms, matched patterns, used globs and directory walks, handled separators and path lists, and resolved symbolic links.
Author
List all Go tutorials.