File I/O
Prerequisites: Working with JSON
The os package handles files. For small files, read or write everything in
one call; for large files or line-by-line processing, open the file and
stream it.
os.ReadFile(path)returns the whole file as[]byte.os.WriteFile(path, data, perm)creates or truncates a file and writesdata.permis a Unix permission such as0o644(ignored for the most part on Windows).os.Open(path)opens a file for reading;os.Create(path)creates or truncates one for writing;os.OpenFilegives full control (append, create, and so on).- Wrap an open file in a
bufio.Scannerto read it line by line. - Always close files you open —
defer f.Close()right after checking the error.
Syntax
data, err := os.ReadFile("notes.txt")
err = os.WriteFile("notes.txt", []byte("hello\n"), 0o644)
f, err := os.Open("notes.txt")
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
}
err = scanner.Err()
Example
package main
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"strings"
)
func main() {
path := "notes.txt"
defer os.Remove(path) // clean up when main returns
// Write the whole file at once.
content := "first line\nsecond line\nthird line\n"
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
log.Fatal(err)
}
// Read the whole file at once.
data, err := os.ReadFile(path)
if err != nil {
log.Fatal(err)
}
fmt.Printf("read %d bytes\n", len(data))
// Append a line.
af, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
log.Fatal(err)
}
fmt.Fprintln(af, "fourth line")
if err := af.Close(); err != nil { // a failed Close can mean lost data
log.Fatal(err)
}
// Stream it line by line.
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
lineNo := 0
for scanner.Scan() {
lineNo++
fmt.Printf("%d: %s\n", lineNo, strings.ToUpper(scanner.Text()))
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
// Detect a missing file.
_, err = os.ReadFile("missing.txt")
fmt.Println(errors.Is(err, os.ErrNotExist))
}
Expected output
read 34 bytes
1: FIRST LINE
2: SECOND LINE
3: THIRD LINE
4: FOURTH LINE
true
Deferred calls run in reverse order, so f.Close() runs before
os.Remove(path) — which matters on Windows, where an open file cannot be
deleted.
Common mistakes
- Forgetting
scanner.Err()after the loop.Scanreturnsfalseboth at the end of the file and on an error. - Very long lines: a
bufio.Scannerhas a 64 KB line limit by default and stops withbufio.Scanner: token too long. Raise it withscanner.Buffer(...)or usebufio.Reader. - Ignoring the error from
Closeon a file you wrote. Buffered data is flushed on close, so that error can mean the write failed. - Relative paths are resolved against the current working directory,
not the location of your
.gofile. defer f.Close()inside a loop that opens many files keeps every file open until the function returns.
Practice
Write the numbers 10, 20 and 30 to nums.txt, one per line. Then open
the file, read it back line by line with a bufio.Scanner, convert each line
with strconv.Atoi, and print the sum.
Show solution
package main
import (
"bufio"
"fmt"
"log"
"os"
"strconv"
)
func main() {
if err := os.WriteFile("nums.txt", []byte("10\n20\n30\n"), 0o644); err != nil {
log.Fatal(err)
}
f, err := os.Open("nums.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
sum := 0
scanner := bufio.NewScanner(f)
for scanner.Scan() {
n, err := strconv.Atoi(scanner.Text())
if err != nil {
log.Fatal(err)
}
sum += n
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
fmt.Println("sum:", sum) // sum: 60
}