ZetCode

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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.