Before this:Interfaces & composition
Working with files & I/O
Key takeaways
Open files with os.Open (read) and os.Create (write), and always
defer f.Close(). The real power is the io.Reader and io.Writer
interfaces — one-method contracts that files, network connections, and buffers all
satisfy, so the same code works on any of them. bufio adds buffering and
convenient line scanning.
Reading and writing data is where the interfaces lesson pays off: Go models every stream through two tiny interfaces. This lesson shows the everyday patterns.
Opening and closing a file
os.Open returns a *os.File (which is an io.Reader) and an error. Pair every open
with a deferred close:
f, err := os.Open("capture.cfile")
if err != nil {
return fmt.Errorf("open capture: %w", err)
}
defer f.Close() // runs when the function returns, even on error
defer guarantees cleanup runs no matter which path the function takes — the reason
you see it on the line right after every successful open.
The two interfaces that tie it together
Almost all of Go’s I/O is built on two one-method interfaces:
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
Because a file, a network socket, a byte buffer, and a gzip stream all satisfy these,
a function written against io.Reader works with every one of them. io.Copy is the
classic example — it pipes any reader into any writer:
n, err := io.Copy(dst, src) // dst is any Writer, src is any Reader
Buffering and scanning lines
Reading a file line by line is a bufio.Scanner:
f, _ := os.Open("channels.txt")
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
line := sc.Text() // one line, no trailing newline
addChannel(line)
}
if err := sc.Err(); err != nil {
return err
}
For writing, wrap the file in a bufio.Writer so many small writes batch into a few
system calls — then Flush before the file closes:
w := bufio.NewWriter(f)
defer w.Flush()
fmt.Fprintf(w, "%s,%.0f\n", ch.Name, ch.FreqHz)
Whole-file shortcuts
When a file is small enough to hold in memory, os.ReadFile and os.WriteFile
collapse open/read/close into one call:
data, err := os.ReadFile("scanlist.json") // []byte of the whole file
err = os.WriteFile("out.json", data, 0o644)
This is how GopherTrunk loads a JSON scan list at startup — read the bytes, then
json.Unmarshal them into the config struct from the
previous lesson.
Quick check: why put defer f.Close() right after opening a file?
Recap
- Open with
os.Open/os.Createanddefer f.Close()immediately. io.Readerandio.Writerare one-method interfaces every stream satisfies, so I/O code composes.bufiobuffers reads/writes and scans lines; remember toFlusha buffered writer.os.ReadFile/os.WriteFilehandle a whole small file in one call.
Next up: Go’s headline feature — launching concurrent work with goroutines.
Frequently asked questions
Why do I always see defer file.Close() in Go?
Because an open file holds an OS resource that must be released. defer schedules Close to run when the function returns — whether it returns normally or early on an error — so the file is always closed and you never leak a handle. It’s the standard Go cleanup pattern.
What's the point of bufio?
Raw file reads and writes each hit the operating system, which is slow when done a byte or line at a time. bufio wraps a reader or writer with an in-memory buffer, so many small operations become a few big ones. bufio.Scanner also makes line-by-line reading trivial.