ZetCode

Go find file

last modified October 30, 2026

In this article we show how to find files in Go. We walk a directory tree with filepath.WalkDir, filter the entries by name, size and modification time, and match paths with glob patterns.

filepath.WalkDir calls a function for every file and directory below a root, in lexical order. The callback receives the path, a fs.DirEntry and an error. Returning fs.SkipDir from the callback skips the rest of a directory, fs.SkipAll stops the whole walk, and any other non-nil error stops the walk and is returned to the caller.

All examples in this article were compiled and run with Go 1.27.0.

The demo directory

The examples search the same small tree, which contains text files at different depths, a small PNG image and a 2 MB binary file. The text files are 10 to 18 bytes long, logo.png has 2048 bytes and backup.bin has 2,000,000 bytes. The modification times are set explicitly: January 10, 2025 for the files at the top and in src, June 1, 2026 for archive/old.txt, and September 15, 2026 for archive/backup.bin and docs/secret.txt.

$ find demo -type f | sort
demo/archive/backup.bin
demo/archive/old.txt
demo/docs/secret.txt
demo/logo.png
demo/notes.txt
demo/src/main.go
demo/src/readme.txt
demo/todo.txt

Finding text files with WalkDir

The first example collects all files with the .txt extension. The extension is taken from the full path, and directories are skipped.

main.go
package main

import (
    "fmt"
    "io/fs"
    "log"
    "path/filepath"
)

func main() {

    var files []string

    err := filepath.WalkDir("demo", func(path string, d fs.DirEntry, err error) error {

        if err != nil {
            return err
        }

        if !d.IsDir() && filepath.Ext(path) == ".txt" {
            files = append(files, path)
        }

        return nil
    })

    if err != nil {
        log.Fatal(err)
    }

    for _, file := range files {
        fmt.Println(file)
    }
}

The callback is called for the root itself, for the subdirectories and for every file. filepath.Ext compares only the suffix, so readme.txt matches while main.go does not. The walk visits the entries in lexical order, which makes the output stable.

$ go run main.go
demo/archive/old.txt
demo/docs/secret.txt
demo/notes.txt
demo/src/readme.txt
demo/todo.txt

Skipping directories

A directory that should not be searched, such as a build output or a .git directory, is pruned by returning fs.SkipDir for it. The walk continues with the next sibling.

main.go
package main

import (
    "fmt"
    "io/fs"
    "log"
    "path/filepath"
)

func main() {

    err := filepath.WalkDir("demo", func(path string, d fs.DirEntry, err error) error {

        if err != nil {
            return err
        }

        if d.IsDir() && d.Name() == "archive" {
            fmt.Println("skipping", path)
            return fs.SkipDir
        }

        if !d.IsDir() {
            fmt.Println(path)
        }

        return nil
    })

    if err != nil {
        log.Fatal(err)
    }
}

archive and its two files are left out; everything else is printed. Note that fs.SkipDir is returned only for the directory itself, not for its children, because the walk never enters it.

$ go run main.go
skipping demo/archive
demo/docs/secret.txt
demo/logo.png
demo/notes.txt
demo/src/main.go
demo/src/readme.txt
demo/todo.txt

Finding files by size

fs.DirEntry does not expose the size, so the example calls d.Info(), which reads the file metadata. Files of at least 1000 bytes are reported with their exact size in bytes.

main.go
package main

import (
    "fmt"
    "io/fs"
    "log"
    "path/filepath"
)

func main() {

    const limit = 1000

    err := filepath.WalkDir("demo", func(path string, d fs.DirEntry, err error) error {

        if err != nil {
            return err
        }

        if d.IsDir() {
            return nil
        }

        info, err := d.Info()
        if err != nil {
            return err
        }

        if info.Size() >= limit {
            fmt.Printf("%8d %s\n", info.Size(), path)
        }

        return nil
    })

    if err != nil {
        log.Fatal(err)
    }
}

The %8d verb right-aligns the size in an eight-character field, so the paths line up. To find files larger than one megabyte, compare against 1024*1024.

$ go run main.go
 2000000 demo/archive/backup.bin
    2048 demo/logo.png

Finding files by modification time

Info.ModTime returns the last modification time. The next example lists text files changed after January 1, 2026.

main.go
package main

import (
    "fmt"
    "io/fs"
    "log"
    "path/filepath"
    "time"
)

func main() {

    limit, err := time.Parse("2006-01-02", "2026-01-01")
    if err != nil {
        log.Fatal(err)
    }

    err = filepath.WalkDir("demo", func(path string, d fs.DirEntry, err error) error {

        if err != nil {
            return err
        }

        if d.IsDir() || filepath.Ext(path) != ".txt" {
            return nil
        }

        info, err := d.Info()
        if err != nil {
            return err
        }

        if info.ModTime().After(limit) {
            fmt.Printf("%s %s\n", info.ModTime().Format("2006-01-02"), path)
        }

        return nil
    })

    if err != nil {
        log.Fatal(err)
    }
}

time.Parse builds the limit from the layout 2006-01-02; the date is midnight UTC. After compares the two instants, and the files from January 2025 are excluded.

$ go run main.go
2026-06-01 demo/archive/old.txt
2026-09-15 demo/docs/secret.txt

Matching names with a regular expression

When the name has to follow a pattern that is not a simple suffix, compile a regular expression once, before the walk. Compiling it inside the callback would repeat the work for every entry.

main.go
package main

import (
    "fmt"
    "io/fs"
    "log"
    "path/filepath"
    "regexp"
)

func main() {

    re := regexp.MustCompile(`o.*\.txt$`)

    err := filepath.WalkDir("demo", func(path string, d fs.DirEntry, err error) error {

        if err != nil {
            return err
        }

        if !d.IsDir() && re.MatchString(d.Name()) {
            fmt.Println(path)
        }

        return nil
    })

    if err != nil {
        log.Fatal(err)
    }
}

The pattern requires an o followed by any characters and the .txt suffix. It matches notes.txt, todo.txt and old.txt, but not readme.txt or main.go.

$ go run main.go
demo/archive/old.txt
demo/notes.txt
demo/todo.txt

Glob patterns are not recursive

filepath.Glob returns the paths that match a shell-style pattern. It looks only at the levels named in the pattern: * does not cross a path separator, and ** has no special meaning in Go.

main.go
package main

import (
    "fmt"
    "log"
    "path/filepath"
)

func main() {

    matches, err := filepath.Glob("demo/*/*.txt")
    if err != nil {
        log.Fatal(err)
    }

    for _, m := range matches {
        fmt.Println(m)
    }

    fmt.Println("---")

    matches, err = filepath.Glob("demo/**/*.txt")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(matches, len(matches))
}

Both patterns return the same three files: demo/**/*.txt behaves like demo/*/*.txt and finds only the files one directory below demo. It does not include demo/notes.txt and does not reach demo/src/readme.txt. A recursive search needs WalkDir.

$ go run main.go
demo/archive/old.txt
demo/docs/secret.txt
demo/src/readme.txt
---
[demo/archive/old.txt demo/docs/secret.txt demo/src/readme.txt] 3

Sorting the results by size

A walk returns files in lexical order, not in the order of their size. To list the largest files, collect the metadata and sort the slice with slices.SortFunc.

main.go
package main

import (
    "fmt"
    "io/fs"
    "log"
    "path/filepath"
    "slices"
)

type file struct {
    path string
    size int64
}

func main() {

    var files []file

    err := filepath.WalkDir("demo", func(path string, d fs.DirEntry, err error) error {

        if err != nil {
            return err
        }

        if d.IsDir() {
            return nil
        }

        info, err := d.Info()
        if err != nil {
            return err
        }

        files = append(files, file{path, info.Size()})
        return nil
    })

    if err != nil {
        log.Fatal(err)
    }

    slices.SortFunc(files, func(a, b file) int {
        return int(b.size - a.size)
    })

    for _, f := range files[:3] {
        fmt.Printf("%8d %s\n", f.size, f.path)
    }
}

The comparator returns a negative number when b is larger, which orders the slice from the largest file to the smallest. The program prints the first three entries.

$ go run main.go
 2000000 demo/archive/backup.bin
    2048 demo/logo.png
      18 demo/todo.txt

Handling errors during the walk

The callback is also called with a non-nil error when an entry cannot be read. Returning that error stops the walk and makes WalkDir return it; returning nil skips the entry and lets the walk continue.

main.go
package main

import (
    "fmt"
    "io/fs"
    "path/filepath"
)

func main() {

    err := filepath.WalkDir("missing", func(path string, d fs.DirEntry, err error) error {
        return err
    })

    fmt.Printf("returning the error: %v\n", err)

    err = filepath.WalkDir("missing", func(path string, d fs.DirEntry, err error) error {
        fmt.Printf("callback: %v\n", err)
        return nil
    })

    fmt.Printf("ignoring the error: %v\n", err)
}

Because the root does not exist, the callback receives an lstat error for it. The first walk passes the error back and WalkDir returns it; the second one prints it and returns nil, so WalkDir reports success even though nothing was visited.

$ go run main.go
returning the error: lstat missing: no such file or directory
callback: lstat missing: no such file or directory
ignoring the error: <nil>

Source

Go path/filepath package - reference
Go io/fs package - reference

In this article we have searched files in Go with filepath.WalkDir and filepath.Glob, filtering by extension, size, modification time and name.

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.