← Go, End to End

Go, End to End

Working with Files and I/O

io.Reader and io.Writer

If Go's standard library has one idea that pays for the whole design, it's these two interfaces:

go
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

That's it. Anything that can produce a stream of bytes implements Reader; anything that can consume one implements Writer. Files, network connections, in-memory buffers, HTTP request/response bodies, compressors, hashers, they all implement these same two interfaces, which means code written against io.Reader/io.Writer works with all of them without modification. A function that copies data doesn't need to know or care whether it's copying from a file to a network socket or from a string to an in-memory buffer.

go · live, editable, runnable

Working with files

The os package is where file operations live: os.Open for reading, os.Create for writing (truncating if it exists), os.OpenFile when you need finer control over flags/permissions. Always pair opening with defer f.Close() right after checking the open succeeded, same pattern as the defer chapter.

go
f, err := os.Open("data.txt")
if err != nil {
	return err
}
defer f.Close()

// f is an io.Reader, use it with anything that accepts one

bufio for buffered, line-based reading

Reading a file byte by byte or in small unbuffered reads is slow (each Read call can mean a system call). bufio.NewReader/bufio.NewScanner wrap a reader with an internal buffer, and Scanner in particular makes line-by-line reading a one-liner:

go · live, editable, runnable

bufio.NewWriter is the write-side equivalent: batches small writes into fewer underlying system calls, but remember to call Flush() before the program exits or you'll lose whatever's still sitting in the buffer.

Try it yourself: use bufio.NewScanner to count how many lines in a multi-line string contain the word "error" (case-insensitive).