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)) // AAAAAThis 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
EasyLet'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
CountingReaderstruct 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 theRead(p []byte) (int, error)method that reads bytes from the source into the provided slice, updates the position, and returnsio.EOFwhen the source is exhausted.Create a
UppercaseWriterstruct that collects written data and converts it to uppercase. It should store the accumulated result internally. Implement theWrite(p []byte) (int, error)method that converts incoming bytes to uppercase and stores them. Add aResult() stringmethod to retrieve the accumulated uppercase text.Create constructor functions
NewCountingReader(source string) *CountingReaderandNewUppercaseWriter() *UppercaseWriterto initialize your types properly.main.go: Demonstrate your custom I/O types working with the standard library.Read a string input, then use your
CountingReaderwithio.ReadAllto read all the data. Pass that data through yourUppercaseWriterusing itsWritemethod.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 WorldYour output should be:
Original: Hello World
Uppercase: HELLO WORLD
Bytes read: 11And given:
Go interfaces are powerfulYour output should be:
Original: Go interfaces are powerful
Uppercase: GO INTERFACES ARE POWERFUL
Bytes read: 26The 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)) // AAAAAThis 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)
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser11Standard Library & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer Interfaceencoding/json with Structshttp.Handler InterfaceRecap - REST API Models3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool