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.Scanreturnsfalseboth at end of file and on an error, and onlyErrtells 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.ScanRunesyields 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:
| Flag | Meaning |
|---|---|
os.O_RDONLY, os.O_WRONLY, os.O_RDWR | open for reading, writing, or both (pick one) |
os.O_CREATE | create the file if it does not exist |
os.O_TRUNC | empty the file when opening |
os.O_APPEND | every write goes to the end |
os.O_EXCL | with 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
| Task | Function |
|---|---|
| create one directory | os.Mkdir(path, 0o755) |
| create a path with parents | os.MkdirAll(path, 0o755) |
| list a directory | os.ReadDir(path) |
| walk a tree | filepath.WalkDir(root, fn) |
| delete a file or empty directory | os.Remove(path) |
| delete a tree | os.RemoveAll(path) |
| rename or move | os.Rename(old, new) |
| temp file or directory | os.CreateTemp("", "prefix-*"), os.MkdirTemp("", "prefix") |
| join path parts | filepath.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 laterRead,WriteorCloseon it returnsinvalid 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
Flushon abufio.Writer. The end of the file is missing. - Ignoring the error from
Closeafter 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.
644is not0o644. Go reads644as the decimal number, which is0o1204and 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".