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
92: Building Custom Readers and Writers
Up until now, we've mostly used the readers and writers provided by the standard library—things like os.File or bytes.Buffer. But the real power of Go's I/O system isn't in the concrete types; it's in the io.Reader and io.Writer interfaces. Because they are so simple, you can wrap them. You can create a "middleware" for your data stream that transforms, filters, or logs data as it passes through.
To see this in action, let's build a Rot13Reader. It's a simple cipher that replaces a letter with the 13th letter after it in the alphabet. Instead of reading a whole file into memory, transforming it, and writing it back, we'll build a reader that transforms the bytes on the fly as they are being read.
Designing the Rot13 Wrapper
The key here is composition. We don't want to implement the low-level logic of reading from a disk or a network socket. Instead, our Rot13Reader will take an existing io.Reader and "wrap" it. Whatever the inner reader produces, we'll modify before handing it to the caller.
type Rot13Reader struct { r io.Reader } func NewRot13Reader(r io.Reader) io.Reader { return &Rot13Reader{r: r} }Now we just need to satisfy the
io.Readerinterface by implementing theReadmethod. This method's job is to fill the provided slicepwith data and return how many bytes were read.Implementing the Transformation Logic
The logic for Rot13 is straightforward: if it's an uppercase letter, shift it within A-Z; if it's lowercase, shift it within a-z. Everything else stays the same.
func (r *Rot13Reader) Read(p []byte) (n int, err error) { n, err = r.r.Read(p) for i := 0; i < n; i++ { b := p[i] if (b >= 'A' && b <= 'Z') { p[i] = (b-'A'+13)%26 + 'A' } else if (b >= 'a' && b <= 'z') { p[i] = (b-'a'+13)%26 + 'a' } } return n, err }Where I messed up the buffer
When I first wrote a similar wrapper, I made a classic mistake. I tried to be "efficient" by creating a temporary buffer inside the
Readmethod to do the transformation, and then I tried to copy that temporary buffer intop.I wrote something like this (don't do this):
// WRONG WAY func (r *Rot13Reader) Read(p []byte) (n int, err error) { temp := make([]byte, len(p)) // Creating a new slice every single call! n, err = r.r.Read(temp) // ... transform temp ... copy(p, temp) return n, err }I quickly realized this was a performance nightmare. If you're reading a 1GB file in 4KB chunks, you're allocating a new 4KB slice thousands of times. The garbage collector would be working overtime. The beauty of the
io.Readerinterface is that the caller provides the bufferp. The correct approach is to modifypin place, which is exactly what I did in the first implementation above. Always respect the buffer provided to you.Wiring it up to a String Reader
To test this, we can wrap a
strings.NewReader. This allows us to treat a simple string as if it were a file or a network stream, making it perfect for unit tests.func main() { input := "Hello, Go World! Let's build some readers." reader := strings.NewReader(input) rotReader := NewRot13Reader(reader) // We use io.ReadAll to consume the custom reader until EOF result, err := io.ReadAll(rotReader) if err != nil { log.Fatal(err) } fmt.Println(string(result)) // Output: Uryyb, Tb Jbeyq! Yrgf ohyna fbev erntebf. }By wrapping the reader, the
io.ReadAllfunction has no idea it's talking to a Rot13 transformer; it just knows it's callingRead()on something that satisfies the interface. This is the "Decorator" pattern in action, and it's how Go handles everything from Gzip compression to TLS encryption.
📋 Practical Task
Exercise: Build a Sensitive Data Masking Writer
Your task is to create a custom io.Writer that masks sensitive information. Instead of a Reader, you will build a MaskingWriter that wraps another io.Writer (like os.Stdout or a bytes.Buffer).
Requirements:
- Define a struct
MaskingWriterthat holds an underlyingio.Writer. - Implement the
Write(p []byte) (n int, err error)method. - In the
Writemethod, look for the byte sequence"SECRET"(case-sensitive). - Replace every occurrence of
"SECRET"with"******"before passing the data to the underlying writer. - Ensure that the number of bytes returned (
n) matches the number of bytes processed from the original slicep, even if you modified the content.
Test your implementation: Wrap a bytes.Buffer with your MaskingWriter, write the string "My SECRET code is 12345" to it, and verify that the buffer contains "My ****** code is 12345".
There are no comments for now.