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
116: strings.Reader and Working with Readers
Why do I even need strings.Reader if I already have the string?
I get this question a lot. If you have a string, you have the data, right? Why wrap it in a "Reader" object? The answer boils down to one of the most important concepts in Go: the io.Reader interface.
In Go, a huge number of functions—everything from HTTP request bodies to JSON decoders and file utilities—don't want a string or a byte slice. They want an io.Reader. They don't care where the data comes from; they just want to be able to call Read() until there's nothing left. If you've written a function that processes data from a file, but now you want to test that function using a hardcoded string in your test file, you can't just pass the string. You use strings.NewReader() to "trick" the function into thinking it's reading from a stream.
Can you show me a real-world example where this is actually useful?
The most common place I use this is with the encoding/json package. Now, you're probably used to json.Unmarshal(), which takes a byte slice. But if you're dealing with larger data or streaming, you'll use json.NewDecoder(). The decoder requires an io.Reader.
Check this out:
package main
import (
"encoding/json"
"fmt"
"strings"
)
type User struct {
Name string `json:"name"`
}
func main() {
// Imagine this string came from a mock API response or a hardcoded config
rawJSON := `{"name": "Alice"}`
// json.NewDecoder needs an io.Reader.
// strings.NewReader converts our string into one.
reader := strings.NewReader(rawJSON)
decoder := json.NewDecoder(reader)
var u User
if err := decoder.Decode(&u); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Decoded user:", u.Name)
}
I prefer this approach over Unmarshal when I'm piping data from one place to another because it's more consistent with how Go handles I/O throughout the rest of the standard library.
Is this better than just converting my string to a byte slice?
You could technically do []byte(myString) and pass that into something that accepts a slice. But here's the catch: converting a string to a byte slice in Go creates a copy of the data. If you have a 10MB string, []byte(s) allocates another 10MB of memory.
strings.Reader is much smarter. It doesn't copy the string; it just keeps track of the current read position (an integer index) and reads from the existing string. It's incredibly efficient. If you're writing high-performance code or dealing with large chunks of text, avoid the cast and use the reader. I've seen production apps leak memory or spike in GC pressure just because someone was casting large strings to byte slices inside a loop.
📋 Practical Task
Exercise: Implementing a Mock Log Parser
You have been handed a function called AnalyzeLogs(r io.Reader) int. This function reads from a stream and counts how many lines contain the word "ERROR". Your task is to write a test main function that uses strings.Reader to pass a multi-line string of mock logs into this function to verify it works.
Requirements:
- Define a multi-line string containing at least three lines: one with "INFO", one with "ERROR", and another with "ERROR".
- Use
strings.NewReaderto wrap that string. - Pass that reader into the provided
AnalyzeLogsfunction. - Print the final count to the console.
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
// AnalyzeLogs is the function you are testing. Do not modify this.
func AnalyzeLogs(r io.Reader) int {
count := 0
scanner := bufio.NewScanner(r)
for scanner.Scan() {
if strings.Contains(scanner.Text(), "ERROR") {
count++
}
}
return count
}
func main() {
// TODO: Implement your mock log string and Reader here
// TODO: Call AnalyzeLogs and print the result
}
There are no comments for now.