Menu

Golang Read File and Write File: os, bufio and Directories

How to read and write files in Go: os.ReadFile and os.WriteFile, reading line by line with bufio.Scanner, appending with os.OpenFile, checking whether a file exists, and working with directories.

This page includes runnable editors - edit, run, and see output instantly.

Read and write a whole file

os.WriteFile and os.ReadFile cover most needs. They open, write or read, and close in one call.

The examples on this page work in a temporary directory from os.MkdirTemp and remove it with defer os.RemoveAll(dir), so they leave nothing behind. In your own code, a relative path like "config.json" is resolved against the process's working directory, which is not necessarily the directory of the source file or the binary.

os.WriteFile creates the file if needed and truncates it if it exists. The third argument is the Unix permission for a newly created file: 0o644 means the owner can read and write, everyone else can read. It is ignored for a file that already exists, and the process umask may remove bits.

os.ReadFile reads everything into memory. That is right for configuration files and small inputs, and wrong for a multi-gigabyte log.

Read line by line with bufio.Scanner

For large files, or when you want lines anyway, use bufio.Scanner. It reads in chunks and hands you one line at a time, without the newline.

Three details:

  • Check sc.Err() after the loop. Scan returns false both at end of file and on an error, and only Err tells them apart.
  • The 64 KB line limit. By default a single line longer than 64 KB stops the scanner with bufio.Scanner: token too long. For files with long lines (minified JSON, some logs), raise the limit before the loop: sc.Buffer(make([]byte, 1024*1024), 10*1024*1024).
  • Other units. sc.Split(bufio.ScanWords) yields words; bufio.ScanRunes yields characters.

For reading a stream in fixed-size chunks rather than lines, use f.Read(buf) in a loop or io.Copy to another writer.

Writing: os.Create, os.OpenFile, and appending

os.Create(name) opens a file for writing, creating or truncating it. os.OpenFile gives full control through flags:

FlagMeaning
os.O_RDONLY, os.O_WRONLY, os.O_RDWRopen for reading, writing, or both (pick one)
os.O_CREATEcreate the file if it does not exist
os.O_TRUNCempty the file when opening
os.O_APPENDevery write goes to the end
os.O_EXCLwith O_CREATE: fail if the file already exists

os.Open(name) is OpenFile(name, O_RDONLY, 0). os.Create(name) is OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0o666).

When writing, the error from Close matters. Some file systems report write failures only at close time, so defer f.Close() alone can hide lost data. For files you write, check Close explicitly as appendLine does. For files you only read, defer f.Close() is fine.

fmt.Fprintln and the rest of the fmt printing functions accept any io.Writer, including a file. A bufio.Writer batches small writes in memory. Forgetting w.Flush() is a classic bug: the program exits normally and the last few kilobytes never reach the file.

Does the file exist?

Go has no os.Exists. Call os.Stat and inspect the error:

errors.Is(err, fs.ErrNotExist) is the current idiom. It replaces the older os.IsNotExist(err), which does not see through wrapped errors.

Checking before opening is often unnecessary and racy: the file can appear or disappear between the check and the open. Usually you just open it and handle fs.ErrNotExist from the open. To create a file only if it does not exist yet, use O_CREATE|O_EXCL, which makes the check and the creation one atomic step.

Directories

TaskFunction
create one directoryos.Mkdir(path, 0o755)
create a path with parentsos.MkdirAll(path, 0o755)
list a directoryos.ReadDir(path)
walk a treefilepath.WalkDir(root, fn)
delete a file or empty directoryos.Remove(path)
delete a treeos.RemoveAll(path)
rename or moveos.Rename(old, new)
temp file or directoryos.CreateTemp("", "prefix-*"), os.MkdirTemp("", "prefix")
join path partsfilepath.Join(a, b, c)

Use path/filepath for file system paths: it uses the right separator for the operating system (\ on Windows). The path package is for slash-separated paths such as URLs.

Go 1.24 also added os.Root (os.OpenRoot(dir)), which opens files only inside one directory and refuses paths that escape it with .. or symlinks. Use it when file names come from users.

Common mistakes

  • Not checking errors. Every one of these calls can fail. A file that failed to open is nil, and every later Read, Write or Close on it returns invalid argument, which hides the real cause (the file was missing, or permission was denied).
  • Forgetting sc.Err() after a scan loop. A read error looks like end of file.
  • Forgetting Flush on a bufio.Writer. The end of the file is missing.
  • Ignoring the error from Close after writing. Write errors can surface only there.
  • defer f.Close() inside a loop over many files. The files stay open until the function returns and you can run out of file descriptors. Move the body into a function so each file closes per iteration.
  • Permissions written as decimal. 644 is not 0o644. Go reads 644 as the decimal number, which is 0o1204 and sets strange bits.

Frequently Asked Questions

How do I read a whole file into a string in Go?

data, err := os.ReadFile("notes.txt") returns the contents as []byte; convert with string(data). It opens, reads and closes the file for you. Use it for files that comfortably fit in memory; for large files, read line by line with bufio.Scanner.

How do I read a file line by line in Go?

Open the file with os.Open, defer f.Close(), wrap it in bufio.NewScanner(f), loop with for sc.Scan() { line := sc.Text() }, and check sc.Err() after the loop. Lines longer than 64 KB make the scanner fail with token too long unless you enlarge its buffer with sc.Buffer.

How do I append to a file in Go?

Open it with os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644), write, and check the error from Close. O_CREATE makes the file if it does not exist, and O_APPEND makes every write go to the end.

How do I check if a file exists in Go?

Call os.Stat(path) and test the error with errors.Is(err, fs.ErrNotExist). A nil error means it exists. Any other error (permission denied, for example) means you cannot tell, so handle it separately rather than treating it as "does not exist".

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED