Go directory
last modified October 30, 2026
In this article we show how to work with directories in Go. We create and remove directories, inspect their permissions, rename them, list their contents and walk a directory tree.
A directory is a unit of a file system that stores files and other
directories. Directories form a tree with parent-child relationships, and a
directory is sometimes called a folder. The examples use the os,
io/fs and path/filepath packages.
All examples were compiled and run with Go 1.27.0 on Linux. The programs create
the files they need, and the walk and glob examples reuse the data
tree built by the listing example.
Creating a directory
os.Mkdir creates one directory with the given permission bits. It
fails if the directory already exists.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
err := os.Mkdir("tmp", 0755)
if err != nil {
fmt.Println(err)
return
}
info, err := os.Stat("tmp")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(info.Name(), info.IsDir())
fmt.Println(info.Mode())
err = os.Mkdir("tmp", 0755)
fmt.Println(err)
fmt.Println(errors.Is(err, fs.ErrExist))
}
The permission 0755 gives the owner read, write and execute
access, and everyone else read and execute access. The second Mkdir
returns an error that wraps fs.ErrExist, which can be tested with
errors.Is.
$ go run main.go tmp true drwxr-xr-x mkdir tmp: file exists true
Creating parent directories
os.MkdirAll creates the directory together with any missing
parents. If the whole path already exists, it does nothing and returns
nil.
package main
import (
"fmt"
"os"
)
func main() {
path := "tmp/doc/new"
for i := 0; i < 2; i++ {
err := os.MkdirAll(path, 0755)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("ok:", path)
}
for _, p := range []string{"tmp", "tmp/doc", "tmp/doc/new"} {
info, err := os.Stat(p)
if err != nil {
fmt.Println(err)
continue
}
fmt.Println(p, info.IsDir(), info.Mode())
}
}
Both calls succeed. The second one finds the complete path and reports no
error, which makes MkdirAll convenient in setup code.
$ go run main.go ok: tmp/doc/new ok: tmp/doc/new tmp true drwxr-xr-x tmp/doc true drwxr-xr-x tmp/doc/new true drwxr-xr-x
Permissions and umask
The mode passed to Mkdir is filtered by the process umask. On a
system with umask 022, a request for 0777 produces a
directory with 0755, because the group and other write bits are
removed.
package main
import (
"fmt"
"os"
)
func main() {
for _, mode := range []os.FileMode{0700, 0755, 0777} {
name := fmt.Sprintf("dir-%o", mode)
os.RemoveAll(name)
if err := os.Mkdir(name, mode); err != nil {
fmt.Println(err)
continue
}
info, err := os.Stat(name)
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("%-8s %s %#o\n", name, info.Mode(), info.Mode().Perm())
}
}
The last line shows the effect of the umask: the requested 0777
is stored as 0755. The Mode method also reports the
file type, while Perm keeps only the permission bits.
$ umask 022 $ go run main.go dir-700 drwx------ 0700 dir-755 drwxr-xr-x 0755 dir-777 drwxr-xr-x 0755
Checking whether a path exists
os.Stat returns a FileInfo or an error. A missing
path produces an error that wraps fs.ErrNotExist, which is more
reliable than comparing error strings.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func check(path string) {
info, err := os.Stat(path)
switch {
case errors.Is(err, fs.ErrNotExist):
fmt.Printf("%-14s does not exist\n", path)
case err != nil:
fmt.Println(err)
case info.IsDir():
fmt.Printf("%-14s is a directory\n", path)
default:
fmt.Printf("%-14s is a file of %d bytes\n", path, info.Size())
}
}
func main() {
os.Mkdir("tmp", 0755)
os.WriteFile("tmp/notes.txt", []byte("hello"), 0644)
check("tmp")
check("tmp/notes.txt")
check("tmp/missing.txt")
}
The function distinguishes a directory, a regular file and a missing path. An
error other than fs.ErrNotExist, such as a permission failure, is
reported separately.
$ go run main.go tmp is a directory tmp/notes.txt is a file of 5 bytes tmp/missing.txt does not exist
Current working directory
os.Getwd returns the absolute path of the current working
directory and os.Chdir changes it. Every relative path in the
program is resolved against that directory.
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
dir, err := os.Getwd()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("working dir:", filepath.Base(dir))
err = os.Chdir("tmp")
if err != nil {
fmt.Println(err)
return
}
dir, err = os.Getwd()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("working dir:", filepath.Base(dir))
}
Only the last element of the path is printed, so the output does not depend on
where the program is started. The directory tmp must exist before
the program runs; mkdir tmp creates it.
$ mkdir tmp $ go run main.go working dir: godir working dir: tmp
Renaming a directory
os.Rename renames a directory or a file. The new path may be in
another directory on the same file system; moving across file systems fails.
package main
import (
"fmt"
"os"
)
func main() {
os.MkdirAll("tmp/doc", 0755)
os.WriteFile("tmp/doc/a.txt", []byte("a"), 0644)
err := os.Rename("tmp", "tmp2")
if err != nil {
fmt.Println(err)
return
}
fmt.Println("renamed")
_, err = os.Stat("tmp2/doc/a.txt")
fmt.Println("content kept:", err == nil)
}
The rename moves the whole subtree, including the file inside
tmp/doc. On Unix, the target must not name an existing
non-empty directory.
$ go run main.go renamed content kept: true
Removing a directory
os.Remove removes one directory, and the directory must be empty.
os.RemoveAll removes the directory, its files and all its
subdirectories.
package main
import (
"fmt"
"os"
)
func main() {
os.MkdirAll("tmp/doc", 0755)
os.WriteFile("tmp/doc/a.txt", []byte("a"), 0644)
err := os.Remove("tmp")
fmt.Println(err)
err = os.RemoveAll("tmp")
fmt.Println(err)
_, err = os.Stat("tmp")
fmt.Println(os.IsNotExist(err))
}
The first call fails because tmp still contains doc.
RemoveAll succeeds and does not complain when the path is already
missing.
$ go run main.go remove tmp: directory not empty <nil> true
Listing a directory
os.ReadDir reads a directory and returns its entries sorted by
file name. Each entry is a fs.DirEntry, which reports the name and
whether the entry is a directory without another system call.
package main
import (
"fmt"
"os"
)
func main() {
os.MkdirAll("data/images", 0755)
os.MkdirAll("data/docs", 0755)
os.WriteFile("data/README.md", []byte("# data\n"), 0644)
os.WriteFile("data/notes.txt", []byte("first note\n"), 0644)
os.WriteFile("data/images/logo.png", []byte("png"), 0644)
os.WriteFile("data/images/photo.jpg", []byte("jpeg"), 0644)
os.WriteFile("data/docs/report.pdf", []byte("%PDF"), 0644)
entries, err := os.ReadDir("data")
if err != nil {
fmt.Println(err)
return
}
for _, e := range entries {
kind := "file"
size := "-"
if e.IsDir() {
kind = "dir "
} else {
info, err := e.Info()
if err != nil {
fmt.Println(err)
continue
}
size = fmt.Sprint(info.Size())
}
fmt.Printf("%s %-12s %s\n", kind, e.Name(), size)
}
}
The listing is not recursive: only the five entries directly inside
data are shown. Uppercase names sort before lowercase names in
byte order, which is why README.md comes first.
$ go run main.go file README.md 7 dir docs - dir images - file notes.txt 11
Walking a directory tree
filepath.WalkDir visits a path and all its descendants in
depth-first order. The callback receives the path, the 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
}
kind := "file"
if d.IsDir() {
kind = "dir "
}
fmt.Printf("%s %s\n", kind, path)
return nil
})
if err != nil {
fmt.Println(err)
}
}
The walk starts at data itself and enters each directory before
its siblings. Returning an error from the callback stops the walk, and the
error is then returned by WalkDir.
$ go run main.go dir data file data/README.md dir data/docs file data/docs/report.pdf dir data/images file data/images/logo.png file data/images/photo.jpg file data/notes.txt
Skipping a subtree
Returning fs.SkipDir for a directory prunes the whole subtree.
This example prints only files and skips the images directory.
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() && d.Name() == "images" {
fmt.Println("skipping", path)
return fs.SkipDir
}
if !d.IsDir() {
fmt.Println(path)
}
return nil
})
if err != nil {
fmt.Println(err)
}
}
SkipDir is returned instead of nil, so the walk does
not descend into data/images. The remaining files are still
visited.
$ go run main.go data/README.md data/docs/report.pdf skipping data/images data/notes.txt
Glob patterns
filepath.Glob returns the paths that match a shell-style pattern.
A pattern without metacharacters is matched literally, ? matches
one character, and [...] matches a character class.
package main
import (
"fmt"
"path/filepath"
)
func main() {
patterns := []string{
"data/*.txt",
"data/*/*.png",
"data/**/*.txt",
"data/[abc",
}
for _, pattern := range patterns {
matches, err := filepath.Glob(pattern)
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("%-16s %v\n", pattern, matches)
}
}
The double star is not special in Go: data/**/*.txt behaves like
data/*/*.txt and searches exactly one directory level below
data, where there is no text file. Recursive matching requires
filepath.WalkDir. An unclosed bracket is reported as a syntax
error, and no matches is not an error: the result is an empty slice.
$ go run main.go data/*.txt [data/notes.txt] data/*/*.png [data/images/logo.png] data/**/*.txt [] syntax error in pattern
Temporary directories
os.MkdirTemp creates a new directory with a random suffix in the
directory given as the first argument, or in os.TempDir when the
argument is empty. The caller is responsible for removing it.
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func main() {
dir, err := os.MkdirTemp("", "work-")
if err != nil {
fmt.Println(err)
return
}
defer os.RemoveAll(dir)
info, err := os.Stat(dir)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("created: ", info.IsDir())
fmt.Println("prefix ok:", strings.HasPrefix(filepath.Base(dir), "work-"))
fmt.Println("mode: ", info.Mode())
err = os.RemoveAll(dir)
fmt.Println("removed: ", err == nil)
_, err = os.Stat(dir)
fmt.Println("exists: ", !os.IsNotExist(err))
}
The directory is created with mode 0700 and a name such as
work-1730291. The suffix is different on every run, so the
program checks only its prefix. The deferred RemoveAll is a no-op
after the explicit removal.
$ go run main.go created: true prefix ok: true mode: drwx------ removed: true exists: false
Source
Go os package - reference
Go path/filepath package - reference
In this article we have worked with directories in Go. We created single and nested directories, inspected permissions and the umask, checked paths, changed the working directory, renamed and removed directories, listed their contents, walked a tree, matched glob patterns and used temporary directories.
Author
List all Go tutorials.