Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
119: Wrapping Readers and Writers for Composability
When you first see io.Reader and io.Writer, they seem almost too simple. They're just interfaces with one method. But the real magic of Go's I/O isn't in the interfaces themselves; it's in how we wrap them. I call this "the onion pattern." You start with a base source of data and keep wrapping it in layers that add specific behaviors.
Why can't I just write a function that handles the decompression and reading in one go?
You could, but you'd be painting yourself into a corner. If you write a function called ReadCompressedFile(path string), that function only works for files. What happens when your boss tells you that the compressed data is now coming from an S3 bucket, or a network socket, or an in-memory buffer for your unit tests?
By wrapping readers, you decouple where the data comes from from what you do to the data. The gzip.NewReader function doesn't care if it's reading from a file or a TCP connection; it just needs something that satisfies io.Reader. This composability is why Go's standard library is so powerful. You build a pipeline of small, single-purpose tools rather than one giant, rigid function.
How do I actually chain multiple wrappers together?
It's a bit like nesting Russian dolls. You pass the result of one wrapper as the input to the next. Let's look at a real-world scenario: you need to read a gzipped log file, but you want to limit the read to the first 10KB to avoid loading a massive file into memory, and you want it buffered for performance.
package main
import (
"bufio"
"compress/gzip"
"fmt"
"io"
"os"
)
func main() {
// 1. The base: a raw file reader
f, err := os.Open("logs.txt.gz")
if err != nil {
panic(err)
}
defer f.Close()
// 2. Wrap it to handle decompression
gzReader, err := gzip.NewReader(f)
if err != nil {
panic(err)
}
defer gzReader.Close()
// 3. Wrap it to limit the number of bytes read (protection against "zip bombs")
limitedReader := io.LimitReader(gzReader, 10240) // 10KB limit
// 4. Wrap it in a buffer to make reads efficient
bufferedReader := bufio.NewReader(limitedReader)
// Now we just use the final wrapper.
// It doesn't know (or care) about the gzip or the limit.
line, _ := bufferedReader.ReadString('\n')
fmt.Println(line)
}
See what happened there? Each layer adds a "capability." The bufio.Reader thinks it's reading from a normal stream, but every time it asks for data, it triggers a chain reaction: it asks the LimitReader, which asks the gzip.Reader, which finally asks the os.File.
Is there a performance cost to adding all these layers?
Technically, yes. Every layer adds a function call to the stack. However, in 99% of Go applications, this is negligible compared to the cost of the actual I/O (like reading from a disk or network). In fact, wrapping is often how you improve performance.
Take bufio as the prime example. Reading one byte at a time from a disk is painfully slow because of system call overhead. By wrapping your reader in a bufio.Reader, you're telling Go to grab a large chunk of data from the disk once and keep it in memory, serving your small reads from that buffer. I've seen developers avoid wrapping because they fear "overhead," only to find their app is 10x slower because they weren't buffering their I/O.
What happens to the original reader or writer when I'm done?
This is where you have to be careful. Not all wrappers are created equal. Some wrappers just pass through the Read or Write calls, but others—like gzip.Reader or bufio.Writer—maintain internal state or buffers that need to be explicitly closed or flushed.
- If a wrapper provides a
Close()method, you should call it. - If you're using a
bufio.Writer, you must callFlush()before closing the underlying writer, otherwise, you'll likely lose the last few KB of data still sitting in the buffer.
My rule of thumb: always defer the Close() of the base resource (the file or socket) and the Close() of any wrapper that requires it, in the reverse order they were created.
📋 Practical Task
Build a "Counting Writer" Wrapper
Your task is to create a custom wrapper for io.Writer that tracks how many total bytes have been written to the underlying stream. This is a common pattern for implementing progress bars or telemetry in CLI tools.
Requirements:
- Define a struct called
ByteCounterthat embeds anio.Writerand has anint64field to track the total bytes. - Implement the
Write([]byte) (int, error)method forByteCounterso that it increments the counter by the number of bytes successfully written by the underlying writer. - In your
mainfunction, wrap anos.Stdout(or a file) with yourByteCounter. - Write several strings to the wrapped writer and then print the final count of bytes written to the console.
There are no comments for now.