Menu
Coddy logo textTech

io.Reader & io.Writer

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 73 of 107.

Go's standard library showcases interface-based design through two fundamental interfaces: io.Reader and io.Writer. These simple interfaces power everything from file operations to network communication.

The io.Reader interface requires just one method:

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

It reads up to len(p) bytes into the slice and returns how many bytes were read. Similarly, io.Writer defines:

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

Any type implementing these methods works with the entire I/O ecosystem.

Here's a custom type that implements io.Reader:

type RepeatReader struct {
    Char  byte
    Count int
    pos   int
}

func (r *RepeatReader) Read(p []byte) (int, error) {
    if r.pos >= r.Count {
        return 0, io.EOF
    }
    n := 0
    for n < len(p) && r.pos < r.Count {
        p[n] = r.Char
        n++
        r.pos++
    }
    return n, nil
}

Now this custom reader works with any function expecting an io.Reader:

reader := &RepeatReader{Char: 'A', Count: 5}
data, _ := io.ReadAll(reader)
fmt.Println(string(data))  // AAAAA

This design lets you write functions that accept io.Reader or io.Writer, making them work with files, network connections, buffers, or any custom implementation.

challenge icon

Challenge

Easy

Let's build a custom text stream processor that implements the io.Reader and io.Writer interfaces! You'll create types that can transform text as it flows through them, demonstrating how these fundamental interfaces enable powerful I/O composition.

You'll organize your code across two files:

  • streams.go: Define your custom reader and writer types.

    Create a CountingReader struct that wraps a string and tracks how many bytes have been read. It should have fields for the source string and a position tracker. Implement the Read(p []byte) (int, error) method that reads bytes from the source into the provided slice, updates the position, and returns io.EOF when the source is exhausted.

    Create a UppercaseWriter struct that collects written data and converts it to uppercase. It should store the accumulated result internally. Implement the Write(p []byte) (int, error) method that converts incoming bytes to uppercase and stores them. Add a Result() string method to retrieve the accumulated uppercase text.

    Create constructor functions NewCountingReader(source string) *CountingReader and NewUppercaseWriter() *UppercaseWriter to initialize your types properly.

  • main.go: Demonstrate your custom I/O types working with the standard library.

    Read a string input, then use your CountingReader with io.ReadAll to read all the data. Pass that data through your UppercaseWriter using its Write method.

    Print the results in this format:

    Original: [input]
    Uppercase: [result from writer]
    Bytes read: [total bytes]

The following input will be provided:

  • A single line of text

For example, given:

Hello World

Your output should be:

Original: Hello World
Uppercase: HELLO WORLD
Bytes read: 11

And given:

Go interfaces are powerful

Your output should be:

Original: Go interfaces are powerful
Uppercase: GO INTERFACES ARE POWERFUL
Bytes read: 26

The key insight is that your CountingReader works seamlessly with io.ReadAll because it implements io.Reader. Any function in Go's ecosystem that accepts an io.Reader will work with your custom type—that's the power of interface-based design.

Cheat sheet

Go's io.Reader and io.Writer interfaces are fundamental to I/O operations:

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

The Read method reads up to len(p) bytes into the slice and returns the number of bytes read. The Write method writes bytes from the slice and returns the number of bytes written.

Any type implementing these methods works with Go's entire I/O ecosystem. Example custom reader:

type RepeatReader struct {
    Char  byte
    Count int
    pos   int
}

func (r *RepeatReader) Read(p []byte) (int, error) {
    if r.pos >= r.Count {
        return 0, io.EOF
    }
    n := 0
    for n < len(p) && r.pos < r.Count {
        p[n] = r.Char
        n++
        r.pos++
    }
    return n, nil
}

Using the custom reader with standard library functions:

reader := &RepeatReader{Char: 'A', Count: 5}
data, _ := io.ReadAll(reader)
fmt.Println(string(data))  // AAAAA

This design allows functions accepting io.Reader or io.Writer to work with files, network connections, buffers, or any custom implementation.

Try it yourself

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	// Read input
	reader := bufio.NewReader(os.Stdin)
	input, _ := reader.ReadString('\n')
	// Remove trailing newline if present
	if len(input) > 0 && input[len(input)-1] == '\n' {
		input = input[:len(input)-1]
	}

	// TODO: Create a CountingReader with the input string
	// countingReader := NewCountingReader(input)

	// TODO: Use io.ReadAll to read all data from your CountingReader
	// data, err := io.ReadAll(countingReader)

	// TODO: Create an UppercaseWriter
	// upperWriter := NewUppercaseWriter()

	// TODO: Write the data to your UppercaseWriter
	// upperWriter.Write(data)

	// TODO: Print the results in the required format:
	// Original: [input]
	// Uppercase: [result from writer]
	// Bytes read: [total bytes]

	// Placeholder to avoid unused import error - remove when implementing
	_ = io.EOF
	fmt.Println("Original:", input)
	// fmt.Println("Uppercase:", upperWriter.Result())
	// fmt.Println("Bytes read:", countingReader.Position)
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming