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.
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.
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.
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.
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.
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.
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.
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.
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
List all Go tutorials.