The Reader Interface
If io.Writer is where bytes go, io.Reader is where they come from. It is
just as small:
type Reader interface {
Read(p []byte) (n int, err error)
}
Read takes a slice you provide, fills it with up to len(p) bytes, and tells
you how many it managed to read (n) and whether anything went wrong (err).
As with Writer, any type with this one method is a Reader — files, network
connections, HTTP request bodies, and in-memory sources all qualify.
EOF is a value, not an exception
The interesting part is how a Reader says "there's nothing left". It doesn't
throw; it returns a specific error value, io.EOF (end of file). In Go, the
end of a stream is normal, so it's reported the same way as any other outcome
— as a value you compare against:
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("hello")
buf := make([]byte, 4)
for {
n, err := r.Read(buf)
fmt.Printf("read %d bytes: %q\n", n, buf[:n])
if err == io.EOF {
break
}
if err != nil {
fmt.Println("error:", err)
break
}
}
}
strings.NewReader(s) wraps a string as a Reader — the quickest way to feed
string data to anything that reads. Notice the loop reads in chunks: Read is
allowed to return fewer bytes than the buffer holds, so you keep calling until
you hit io.EOF. Treating EOF as an error value, rather than a special case in
the type system, is a deliberate choice: end-of-input flows through the same
if err != nil plumbing as everything else.
You rarely call Read yourself
That loop is instructive, but you almost never write it. The standard library already did, and wrapped the result in helpers that read against any Reader.
The bluntest one slurps the whole stream into a byte slice:
data, err := io.ReadAll(r) // one []byte, or an error
io.ReadAll runs the read loop for you and stops cleanly at EOF (it does not
return io.EOF as an error — reaching the end is success). Convenient, but it
holds the entire input in memory, so reserve it for things you know are small.
Reading line by line
For text, the workhorse is bufio.Scanner. It splits a Reader into tokens —
by default, lines — and hands them to you one at a time:
package main
import (
"bufio"
"fmt"
"strings"
)
func main() {
r := strings.NewReader("first\nsecond\nthird")
scanner := bufio.NewScanner(r)
for scanner.Scan() {
fmt.Println("line:", scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Println("scan error:", err)
}
}
scanner.Scan() advances to the next line and returns false when there's
nothing more — either the stream ended or an error occurred. Because a false
covers both, you check scanner.Err() afterward: it returns nil for a clean
end and the real error otherwise. (EOF is a clean end, so it never shows up
here.)
One subtlety the exercise leans on: a trailing newline does not create an
extra empty line. "a\nb\n" scans as two lines, not three — the final \n
ends the last line rather than starting a new empty one.
Your task is to write countLines(r io.Reader) using a Scanner. Because it
takes an io.Reader, the same function will count lines in a string, a file,
or a network stream — you write it once.
Practice
1. Count the Lines
IntermediateCount the lines in any stream of text. Implement
countLines:func countLines(r io.Reader) (int, error)- Read
rline by line with abufio.Scanner(bufio.NewScanner(r)). - Every time
scanner.Scan()returnstrue, you've consumed one more line — increment a counter. - When scanning stops, return the count along with
scanner.Err()(which isnilon a clean end-of-stream, so it doubles as your error return).
Because the parameter is an
io.Reader, the same function counts lines in a string, a file, or a network connection — the tests just passstrings.NewReader(...).Watch the edge cases:
- An empty string has
0lines. - A trailing newline does not add an empty line:
"a\nb\n"is2lines, not3. TheScannerhandles this for you — don't add special-case logic.
package main import ( "fmt" "io" "strings" ) // countLines reports how many lines the reader r contains, using a // bufio.Scanner. It returns the count and scanner.Err() (nil on a clean end). func countLines(r io.Reader) (int, error) { // TODO: implement — scan r line by line and count the lines return 0, nil } func main() { n, err := countLines(strings.NewReader("first\nsecond\nthird")) fmt.Println(n, err) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Read