Directories & File System — filepath·fs·embed Packages
Working with a single file is very different from traversing the entire file system. Go uses path/filepath for path manipulation, the fs package to abstract virtual file systems, and embed to bundle files into binaries — three pillars for complete file system support.
path/filepath Package — Platform-Independent Path Handling
The filepath package automatically handles OS path separators (Windows \, Unix /). Never concatenate paths as strings directly — always use filepath functions.
Path Manipulation Functions
package main
import (
"fmt"
"path/filepath"
)
func main() {
// Join: combine path segments with the OS-appropriate separator
p := filepath.Join("home", "user", "documents", "file.txt")
fmt.Println("Join:", p) // Linux: home/user/documents/file.txt
// Dir, Base, Ext: decompose a path
fmt.Println("Dir: ", filepath.Dir(p)) // home/user/documents
fmt.Println("Base:", filepath.Base(p)) // file.txt
fmt.Println("Ext: ", filepath.Ext(p)) // .txt
// extract filename without extension
base := filepath.Base(p)
name := base[:len(base)-len(filepath.Ext(base))]
fmt.Println("name (no ext):", name) // file
// Abs: convert to absolute path
abs, err := filepath.Abs("relative/path")
if err == nil {
fmt.Println("absolute path:", abs)
}
// Rel: compute relative path
rel, err := filepath.Rel("/home/user", "/home/user/docs/file.txt")
if err == nil {
fmt.Println("relative path:", rel) // docs/file.txt
}
// Clean: normalize a path
messy := filepath.Join("a", "..", "b", ".", "c")
fmt.Println("Clean:", filepath.Clean(messy)) // b/c
// Split: separate directory and file name
dir, file := filepath.Split("/home/user/file.txt")
fmt.Printf("Split → dir=%q, file=%q\n", dir, file)
}
filepath.Walk and WalkDir
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func main() {
// Create test directory structure
os.MkdirAll("testdir/sub1", 0755)
os.MkdirAll("testdir/sub2", 0755)
os.WriteFile("testdir/a.go", []byte("package main"), 0644)
os.WriteFile("testdir/sub1/b.go", []byte("package sub1"), 0644)
os.WriteFile("testdir/sub2/c.txt", []byte("text"), 0644)
fmt.Println("=== filepath.WalkDir (Go 1.16+, more efficient) ===")
err := filepath.WalkDir("testdir", func(path string, d os.DirEntry, err error) error {
if err != nil {
return err // handle access errors
}
// skip hidden directories
if d.IsDir() && strings.HasPrefix(d.Name(), ".") {
return filepath.SkipDir // skip entire directory
}
indent := strings.Repeat(" ", strings.Count(path, string(os.PathSeparator)))
if d.IsDir() {
fmt.Printf("%s[DIR] %s\n", indent, d.Name())
} else {
info, _ := d.Info()
fmt.Printf("%s[FILE] %s (%d bytes)\n", indent, d.Name(), info.Size())
}
return nil
})
if err != nil {
fmt.Println("WalkDir error:", err)
}
// cleanup
os.RemoveAll("testdir")
}
filepath.Glob — Pattern Matching
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
// Create test files
os.WriteFile("main.go", []byte(""), 0644)
os.WriteFile("util.go", []byte(""), 0644)
os.WriteFile("readme.md", []byte(""), 0644)
os.WriteFile("config.yaml", []byte(""), 0644)
// Glob: list files matching a pattern
goFiles, _ := filepath.Glob("*.go")
fmt.Println("Go files:", goFiles) // [main.go util.go]
// Match: check if a single path matches a pattern
matched, _ := filepath.Match("*.go", "main.go")
fmt.Println("main.go matches *.go:", matched) // true
matched, _ = filepath.Match("*.go", "readme.md")
fmt.Println("readme.md matches *.go:", matched) // false
// Recursive pattern (** is NOT supported — use filepath.Walk instead)
allGo, _ := filepath.Glob("**/*.go") // returns nil
fmt.Println("** pattern result:", allGo) // [] — not supported
// cleanup
for _, f := range []string{"main.go", "util.go", "readme.md", "config.yaml"} {
os.Remove(f)
}
}
fs Package — Virtual File System Abstraction (Go 1.16+)
The fs.FS interface lets you work with real disks, embedded files, compressed archives, and remote storage all in the same way.
package main
import (
"fmt"
"io/fs"
"os"
)
func main() {
// Prepare test directory
os.MkdirAll("fstest/sub", 0755)
os.WriteFile("fstest/hello.txt", []byte("Hello"), 0644)
os.WriteFile("fstest/sub/world.txt", []byte("World"), 0644)
// os.DirFS: convert a real directory to fs.FS
fsys := os.DirFS("fstest")
// fs.ReadFile: read a file from fs.FS
data, err := fs.ReadFile(fsys, "hello.txt")
if err != nil {
fmt.Println("read failed:", err)
return
}
fmt.Println("content:", string(data))
// fs.ReadDir: list directory entries
entries, _ := fs.ReadDir(fsys, ".")
fmt.Println("\nroot directory contents:")
for _, e := range entries {
fmt.Printf(" %s (dir: %v)\n", e.Name(), e.IsDir())
}
// fs.Stat: query file info
info, _ := fs.Stat(fsys, "hello.txt")
fmt.Printf("\nhello.txt size: %d bytes\n", info.Size())
// fs.WalkDir: traverse entire fs.FS
fmt.Println("\nall files:")
fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Printf(" %s\n", path)
return nil
})
os.RemoveAll("fstest")
}
os Package — Directory Management
package main
import (
"fmt"
"os"
)
func main() {
// MkdirAll: create directories including all intermediates
err := os.MkdirAll("project/src/models", 0755)
if err != nil {
fmt.Println("mkdir failed:", err)
return
}
// ReadDir: list directory contents (one level only)
entries, err := os.ReadDir("project")
if err != nil {
fmt.Println("ReadDir failed:", err)
return
}
fmt.Println("project contents:")
for _, e := range entries {
fmt.Printf(" %s (dir=%v)\n", e.Name(), e.IsDir())
}
// CreateTemp: create a temp file with a unique name
tmpFile, err := os.CreateTemp("", "myapp-*.tmp")
if err != nil {
fmt.Println("temp file creation failed:", err)
return
}
fmt.Println("temp file:", tmpFile.Name())
tmpFile.WriteString("temporary data")
tmpFile.Close()
os.Remove(tmpFile.Name()) // cleanup after use
// MkdirTemp: create a temp directory
tmpDir, err := os.MkdirTemp("", "myapp-*")
if err != nil {
fmt.Println("temp dir creation failed:", err)
return
}
fmt.Println("temp directory:", tmpDir)
defer os.RemoveAll(tmpDir) // cleanup after use
// RemoveAll: remove directory and all its contents
os.RemoveAll("project")
fmt.Println("project removed")
}
embed Package — Bundle Files into the Binary (Go 1.16+)
Using the //go:embed directive, you can include files in the Go binary at build time. This enables single-binary servers that need no external files at deployment.
package main
import (
"embed"
"fmt"
"io/fs"
"net/http"
)
// embed a single file
//
//go:embed config.yaml
var configData []byte
// embed as a string
//
//go:embed version.txt
var version string
// embed an entire directory (requires embed.FS type)
//
//go:embed static
var staticFiles embed.FS
func main() {
// print embedded file content
fmt.Println("version:", version)
fmt.Println("config file size:", len(configData), "bytes")
// read a file from embed.FS
data, err := staticFiles.ReadFile("static/index.html")
if err != nil {
fmt.Println("embedded file read failed:", err)
} else {
fmt.Println("index.html content:", string(data))
}
// serve embed.FS via http.FileServer
// set the static/ subdirectory as root
webRoot, err := fs.Sub(staticFiles, "static")
if err != nil {
fmt.Println("fs.Sub failed:", err)
return
}
http.Handle("/", http.FileServer(http.FS(webRoot)))
fmt.Println("server starting: http://localhost:8080")
// http.ListenAndServe(":8080", nil)
}
embed Package Notes:
- The
//go:embedcomment must be directly followed by the variable declaration embed.FSis read-only (no writes allowed)- Hidden files (starting with
.) and files starting with_are excluded by default - Use the
all:prefix to include hidden files://go:embed all:static
Real-World Example 1 — Recursive Directory Explorer
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
type FileStats struct {
TotalFiles int
TotalDirs int
TotalSize int64
ByExt map[string]int
}
// exploreDirectory: recursively explore a directory and collect statistics
func exploreDirectory(root string) (*FileStats, error) {
stats := &FileStats{ByExt: make(map[string]int)}
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
// skip directories we can't access
fmt.Printf("access denied: %s (%v)\n", path, err)
return nil
}
if d.IsDir() {
// exclude hidden directories
if strings.HasPrefix(d.Name(), ".") && path != root {
return filepath.SkipDir
}
stats.TotalDirs++
return nil
}
stats.TotalFiles++
info, err := d.Info()
if err == nil {
stats.TotalSize += info.Size()
}
ext := strings.ToLower(filepath.Ext(d.Name()))
if ext == "" {
ext = "(no extension)"
}
stats.ByExt[ext]++
return nil
})
return stats, err
}
func printStats(stats *FileStats) {
fmt.Printf("\nDirectory Exploration Results\n")
fmt.Printf("─────────────────────────────\n")
fmt.Printf("files: %d\n", stats.TotalFiles)
fmt.Printf("directories: %d\n", stats.TotalDirs)
fmt.Printf("total size: %.2f KB\n", float64(stats.TotalSize)/1024)
// sort by extension
type extCount struct {
ext string
count int
}
var sorted []extCount
for ext, cnt := range stats.ByExt {
sorted = append(sorted, extCount{ext, cnt})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].count > sorted[j].count
})
fmt.Println("\nfiles by extension:")
for _, ec := range sorted {
fmt.Printf(" %-20s %d\n", ec.ext, ec.count)
}
}
func main() {
// Create test directory structure
os.MkdirAll("project/src", 0755)
os.MkdirAll("project/docs", 0755)
os.MkdirAll("project/.git", 0755) // hidden directory
os.WriteFile("project/src/main.go", []byte("package main"), 0644)
os.WriteFile("project/src/util.go", []byte("package main"), 0644)
os.WriteFile("project/docs/README.md", []byte("# Docs"), 0644)
os.WriteFile("project/go.mod", []byte("module project"), 0644)
os.WriteFile("project/.git/HEAD", []byte("ref: refs/heads/main"), 0644)
stats, err := exploreDirectory("project")
if err != nil {
fmt.Println("exploration failed:", err)
return
}
printStats(stats)
os.RemoveAll("project")
}
Real-World Example 2 — Embedded Static File Web Server
A static file server using the embed package. Works anywhere with just a single executable.
//go:build ignore
package main
import (
"embed"
"fmt"
"io/fs"
"log"
"net/http"
"time"
)
//go:embed web
var webFiles embed.FS
func main() {
// serve web/ directory as root
webRoot, err := fs.Sub(webFiles, "web")
if err != nil {
log.Fatal("web root setup failed:", err)
}
mux := http.NewServeMux()
// serve static files
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(webRoot))))
// API endpoint
mux.HandleFunc("/api/version", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"version":"1.0.0","built":"%s"}`, time.Now().Format(time.RFC3339))
})
// SPA fallback: route all paths to index.html
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data, err := fs.ReadFile(webFiles, "web/index.html")
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(data)
})
addr := ":8080"
fmt.Printf("server starting: http://localhost%s\n", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
Pro Tips
filepath vs path: filepath uses OS-appropriate separators, while path (slash-only) is for URL paths. Always use filepath for file system paths.
WalkDir is more efficient than Walk: filepath.Walk calls os.Lstat on every entry, but filepath.WalkDir (Go 1.16+) reuses DirEntry to reduce unnecessary syscalls. Always use WalkDir in new code.
embed.FS is determined at compile time: You cannot add or modify files at runtime. A common pattern is to use the real file system during development and activate embed only for production builds — this improves development efficiency.
Use RemoveAll with caution: os.RemoveAll with a wrong path can permanently delete data. Always validate the path explicitly before calling it.