Skip to Content
Course content

116: strings.Reader and Working with Readers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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.NewReader to wrap that string.
  • Pass that reader into the provided AnalyzeLogs function.
  • 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
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.