Filesystem and path/filepath
Filesystem and path/filepath
Manipulating directories, reading files, and resolving paths are foundational for desk utilities and servers. Go provides cross-platform path handling in path/filepath and filesystem utilities in os. The boring default is: always use filepath (not string concatenation with "/"), use os.ReadFile and os.WriteFile for small files, and walk trees with filepath.WalkDir.
Mental model
Windows uses \ as a path separator, while Linux/macOS use /. If you join paths with dir + "/" + file, your code breaks on Windows. filepath.Join handles OS-specific path separators automatically.
filepath.Clean resolves . and .. components and strips redundant slashes. filepath.Rel computes relative paths between two targets. filepath.WalkDir iterates through a directory hierarchy efficiently, skipping stat calls when possible (superior to legacy filepath.Walk).
Use os.ReadFile(path) and os.WriteFile(path, data, perm) for self-contained file operations when streaming is unnecessary.
Worked examples
Case 1: Cross-platform filepath.Join and Clean
Save as desk_paths.go. Clean and join file paths safely.
// desk_paths.go
package main
import (
"fmt"
"path/filepath"
)
func main() {
p := filepath.Join("reports", "2026", "september", "..", "august", "orders.csv")
fmt.Println("joined & cleaned:", filepath.ToSlash(p))
fmt.Println("base filename:", filepath.Base(p))
fmt.Println("parent directory:", filepath.ToSlash(filepath.Dir(p)))
fmt.Println("extension:", filepath.Ext(p))
}Run:
go run desk_paths.goOutput:
joined & cleaned: reports/2026/august/orders.csv
base filename: orders.csv
parent directory: reports/2026/august
extension: .csv
filepath.ToSlash normalizes slashes to / (helpful for platform-neutral logs and output). filepath.Base extracts the last element; filepath.Dir extracts the directory path.
Case 2: Reading and writing files atomically with os
Save as file_ops.go. Write a file, verify its existence, read it back, and remove it cleanly.
// file_ops.go
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
dir, err := os.MkdirTemp("", "desk-files-*")
if err != nil {
fmt.Println("err:", err)
return
}
defer os.RemoveAll(dir)
target := filepath.Join(dir, "shift_notes.txt")
content := []byte("morning shift: all tables cleared\n")
if err := os.WriteFile(target, content, 0644); err != nil {
fmt.Println("write err:", err)
return
}
data, err := os.ReadFile(target)
if err != nil {
fmt.Println("read err:", err)
return
}
fmt.Print(string(data))
}Run:
go run file_ops.goOutput:
morning shift: all tables cleared
os.WriteFile truncates and writes the file in one shot with requested permissions. os.MkdirTemp creates a dedicated sandbox for temporary files.
Case 3: Walking directory trees with filepath.WalkDir
Save as walk_desk.go. Traverse nested folders and inspect file entries.
// walk_desk.go
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
)
func main() {
tmp, err := os.MkdirTemp("", "desk-tree-*")
if err != nil {
fmt.Println(err)
return
}
defer os.RemoveAll(tmp)
// Create nested files
os.MkdirAll(filepath.Join(tmp, "archive", "sub"), 0755)
os.WriteFile(filepath.Join(tmp, "orders.txt"), []byte("ok"), 0644)
os.WriteFile(filepath.Join(tmp, "archive", "receipt.csv"), []byte("ok"), 0644)
var files []string
err = filepath.WalkDir(tmp, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
rel, _ := filepath.Rel(tmp, path)
files = append(files, filepath.ToSlash(rel))
}
return nil
})
if err != nil {
fmt.Println("walk err:", err)
return
}
fmt.Println("found files:", len(files))
for _, f := range files {
fmt.Println("file:", f)
}
}Run:
go run walk_desk.goOutput:
found files: 2
file: archive/receipt.csv
file: orders.txt
fs.DirEntry supplies metadata without triggering unnecessary stat calls on every path, making WalkDir fast on large trees.
The trap
Save as slash_concat.go. Concatenating file paths with string additions or / creates broken paths on Windows and fails to handle boundary slashes properly.
// slash_concat.go
package main
import (
"fmt"
"path/filepath"
)
func main() {
dir := "records/"
file := "/ticket.json"
// Wrong: manual string concat results in redundant slash
badPath := dir + "/" + file
// Right: filepath.Join sanitizes and handles separators correctly
goodPath := filepath.Join(dir, file)
fmt.Println("bad:", badPath)
fmt.Println("good:", filepath.ToSlash(goodPath))
}Run:
go run slash_concat.goOutput:
bad: records///ticket.json
good: records/ticket.json
Use filepath.Join to remove duplicate separators and resolve relative segments safely.
The boring rule
- Always use
path/filepathfor filesystem paths; reservepathstrictly for URL paths. - Use
filepath.Joininstead of string formatting or+ "/"concatenation. - Use
os.ReadFileandos.WriteFilefor small files; useos.Open/bufiofor large streams. - Use
filepath.WalkDirinstead offilepath.Walk. - Use
os.MkdirTempandos.RemoveAllto isolate tests and temporary file operations.
Try this
- In
desk_paths.go, addfilepath.Match("*.csv", "orders.csv")and check if it matches. - In
walk_desk.go, returnfilepath.SkipDirwhend.Name() == "archive"and observe that files insidearchive/are skipped. - Use
os.Stat(target)infile_ops.goand printinfo.Size().