Skip to content

io and bufio

The io package defines two small but foundational interfaces:

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

These two interfaces are the lingua franca of I/O in Go. Dozens of types implement them: files, network connections, HTTP request and response bodies, in-memory buffers, and more. Functions that accept io.Reader or io.Writer work with all of them without modification.

strings.NewReader(s) wraps a string as an io.Reader. This is invaluable in tests and examples — you can exercise any function that reads from a stream without touching the filesystem.

r := strings.NewReader("hello, world")
data, _ := io.ReadAll(r)
fmt.Println(string(data)) // hello, world

bytes.Buffer implements both io.Reader and io.Writer. You write into it and read back out:

var buf bytes.Buffer
fmt.Fprintln(&buf, "first line")
fmt.Fprintln(&buf, "second line")
content, _ := io.ReadAll(&buf)
fmt.Print(string(content))

fmt.Fprintln accepts any io.Writer, so you can redirect formatted output to a buffer, a file, or a network connection with zero code changes.

bufio.Scanner wraps any io.Reader and provides convenient line-by-line (or token-by-token) reading. The default split function is bufio.ScanLines.

scanner := bufio.NewScanner(r)
for scanner.Scan() {
fmt.Println(scanner.Text()) // one line at a time
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}

scanner.Scan() returns false when the input is exhausted or an error occurs. Always check scanner.Err() after the loop — a false return alone does not distinguish EOF from an error.

The io package also provides useful helpers:

io.ReadAll(r) // read until EOF, return []byte
io.Copy(dst, src) // copy from Reader to Writer
io.MultiReader(r1, r2) // chain readers sequentially
io.Discard // Writer that discards all bytes (like /dev/null)
io.LimitReader(r, n) // read at most n bytes
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
)
// printLines accepts any io.Reader — works with files, buffers, or strings
func printLines(r io.Reader) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
fmt.Println(">", scanner.Text())
}
}
func main() {
// strings.NewReader implements io.Reader
r := strings.NewReader("first line\nsecond line\nthird line")
printLines(r)
// bytes.Buffer implements both io.Reader and io.Writer
var buf bytes.Buffer
fmt.Fprintln(&buf, "written to buffer")
fmt.Fprintln(&buf, "another line")
content, _ := io.ReadAll(&buf)
fmt.Print(string(content))
// io.MultiReader chains two readers sequentially
r1 := strings.NewReader("hello ")
r2 := strings.NewReader("world\n")
combined := io.MultiReader(r1, r2)
io.Copy(io.Discard, combined)
fmt.Println("MultiReader composed two readers")
// bufio.Scanner over in-memory CSV
csv := "alice,30\nbob,25\ncharlie,35"
scanner := bufio.NewScanner(strings.NewReader(csv))
for scanner.Scan() {
parts := strings.Split(scanner.Text(), ",")
fmt.Printf("name=%s age=%s\n", parts[0], parts[1])
}
}
What does io.ReadAll(r) return?
After bufio.Scanner.Scan() returns false, what must you check?
Which type implements both io.Reader and io.Writer?
Why accept io.Reader in a function instead of *os.File?