← thecodex.expert · The Codex Family of Knowledge
Go

IO and Files

Two one-method interfaces — io.Reader and io.Writer — are why the same code can read a file, a network socket, or an in-memory buffer without changes.

Go 1.26 io.Reader / io.Writer Last verified:
Where this bites you

You scan a file line by line with bufio.Scanner, exactly like the example later on this page — and it works, for months, on every file you test it with. Then it meets one real file with one unusually long line, and the loop just... stops. No panic, no error printed, nothing in the logs. It looks like the file was empty.

Goio_bug.go
scanner := bufio.NewScanner(f)
lines := 0
for scanner.Scan() {
	lines++
	fmt.Println("line", lines)
}
// no scanner.Err() check -- the loop just stops, no error visible
fmt.Println("done, read", lines, "lines")
// done, read 0 lines   -- a 100KB line silently produced ZERO output, no error
Canonical Definition

The os package opens, creates, and manipulates files as *os.File values, which themselves implement the tiny io.Reader (one method: Read) and io.Writer (one method: Write) interfaces. Because so many types across the standard library implement these same two interfaces — network connections, in-memory buffers, compressors — code written against io.Reader/io.Writer works unchanged regardless of where the bytes actually come from or go.

Reading a whole file

For small-to-medium files, os.ReadFile reads the entire contents in one call — the simplest option when you don't need to stream.

Goread_file.go
data, err := os.ReadFile("config.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

Opening a file and using defer to close it

For more control (streaming large files, writing, appending), os.Open/os.Create return an *os.File that must be explicitly closed — idiomatically with defer right after the open succeeds, guaranteeing cleanup regardless of how the function returns.

Goopen_file.go
f, err := os.Open("data.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()   // guaranteed to run when this function returns

scanner := bufio.NewScanner(f)
for scanner.Scan() {
    fmt.Println(scanner.Text())   // one line at a time, memory-efficient
}

The example above is exactly the shape of the callout at the top of this page — and it has exactly the same gap. scanner.Scan() returns false both when it reaches the end of the file (fine) and when it hits a real error, including a line longer than its default 64KB buffer (not fine, and silent unless you check). scanner.Err() is what tells the two apart, and it has to be checked after the loop ends, not inside it:

Goio_fix.go
scanner := bufio.NewScanner(f)
lines := 0
for scanner.Scan() {
	lines++
}
if err := scanner.Err(); err != nil {
	log.Fatal(err) // now the failure is visible
}
fmt.Println("done, read", lines, "lines")
// 2026/10/04 ... bufio.Scanner: token too long

Writing files

os.WriteFile writes a full byte slice in one call, creating the file if needed; os.Create plus Write gives more control for streaming output.

Gowrite_file.go
err := os.WriteFile("output.txt", []byte("hello\n"), 0644)
if err != nil {
    log.Fatal(err)
}

Sources

1
The Go Authors. io package and os package documentation, pkg.go.dev/io and pkg.go.dev/os.