Skip to content

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.

2 min read
  • go
  • snippets

This article was written in 2021. Since Go 1.16, the helpers live under os instead of io/ioutil (os.ReadFile, os.WriteFile); the io/ioutil package 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:

  1. If its extension is .ts or .tsx and the first line is // @deprecated, add it to the deprecated counter.
  2. If its extension is .py, prepend # file modified as 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.