Reading and writing files and directories in Go
Minimal recipes for reading a file, overwriting one, and walking a directory tree in Go, with a small end-to-end example that ties the three together.
This article was written in 2021. Since Go 1.16, the helpers live under
osinstead ofio/ioutil(os.ReadFile,os.WriteFile); theio/ioutilpackage is deprecated but still works. The snippets below already use the modern API.
A while back I needed to walk thousands of files and folders, read their contents, and count how many matched a certain condition. My first instinct was Node, but after looking at the equivalent packages in Go I ended up picking Go — the API is more direct and the resulting binary ships without a runtime.
Here are the snippets I keep reaching for.
Read a file
package main
import (
"fmt"
"os"
)
const filePath = "file.txt"
func main() {
content, err := os.ReadFile(filePath)
if err != nil {
panic(err)
}
fmt.Println("File content:", string(content))
}Create or overwrite a file
os.WriteFile creates the file if it doesn't exist and truncates it if it does.
package main
import (
"os"
)
const filePath = "./file.txt"
func main() {
newContent := []byte("file was modified")
// 0644 = rw for owner, r for group and others.
// Handy calculator: https://chmod-calculator.com
if err := os.WriteFile(filePath, newContent, 0644); err != nil {
panic(err)
}
}Walk a directory recursively
filepath.WalkDir is the modern flavor of filepath.Walk: it uses fs.DirEntry, which avoids an extra os.Stat per file.
package main
import (
"fmt"
"io/fs"
"path/filepath"
)
const folderPath = "/my/folder/path"
func main() {
err := filepath.WalkDir(folderPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Printf("found: %s\n", d.Name())
return nil
})
if err != nil {
panic(err)
}
}Putting it together
Let's combine the three helpers to solve a real task:
Walk a folder tree. For each file:
- If its extension is
.tsor.tsxand the first line is// @deprecated, add it to the deprecated counter. - If its extension is
.py, prepend# file modifiedas the new first line.
Extra rule: skip any folder named tmp.
At the end, print a summary.
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
const folderPath = "/my/folder/path"
func main() {
var modifiedFiles, deprecatedFiles int
err := filepath.WalkDir(folderPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Skip any folder named "tmp"
if d.IsDir() && d.Name() == "tmp" {
return filepath.SkipDir
}
if d.IsDir() {
return nil
}
switch filepath.Ext(d.Name()) {
case ".py":
content, err := os.ReadFile(path)
if err != nil {
return err
}
newContent := append([]byte("# file modified\n"), content...)
if err := os.WriteFile(path, newContent, 0644); err != nil {
return err
}
modifiedFiles++
case ".ts", ".tsx":
content, err := os.ReadFile(path)
if err != nil {
return err
}
firstLine, _, _ := strings.Cut(string(content), "\n")
if strings.TrimSpace(firstLine) == "// @deprecated" {
deprecatedFiles++
}
}
return nil
})
if err != nil {
panic(err)
}
fmt.Println("Total files with extension .py modified: ", modifiedFiles)
fmt.Println("Total files with the \"@deprecated\" comment:", deprecatedFiles)
}Wrap-up
Go's standard library covers most filesystem work without extra dependencies, and the API feels coherent once you've internalized os, io/fs, and path/filepath. For one-off scripts that touch thousands of files, it ended up being a much more predictable choice than the Node equivalent.
Thanks for reading.