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.

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

Practice on your own: Online Go compiler