DEV Community

Cover image for The Secret Life of Go: Interfaces in Practice
Aaron Rose
Aaron Rose

Posted on

The Secret Life of Go: Interfaces in Practice

How to replace three redundant functions with one io.Reader.


Chapter 18: The Universal Adapter

The archive was quiet, except for the rhythmic thrum-click of the pneumatic tube system delivering requests to the front desk. Ethan had his headphones on, typing furiously.

"You are typing very fast," Eleanor observed, pausing at his desk with a cart full of magnetic tapes. "That usually means you are copying and pasting."

Ethan pulled off his headphones, looking guilty. "I'm building a log analyzer. It needs to read logs from three places: a local file archive, a live HTTP stream from the server, and sometimes just a raw string for testing."

He pointed to his code. "I wrote three functions."

// 1. Read from a file
func AnalyzeFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()

    data, _ := io.ReadAll(f) // Read everything into memory
    return processLog(data)
}

// 2. Read from the web
func AnalyzeWebStream(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    data, _ := io.ReadAll(resp.Body)
    return processLog(data)
}

// 3. Read from a string (for tests)
func AnalyzeString(logs string) error {
    return processLog([]byte(logs))
}

Enter fullscreen mode Exit fullscreen mode

"It works," Ethan defended. "But it feels... repetitive."

"It is repetitive," Eleanor agreed. "Because you are writing code for things instead of behaviors."

The Behavior

She picked up a cable from his desk. It was a standard USB-C charger. "What does this plug into?"

"My phone," Ethan said. "Or my laptop. Or your tablet."

"Exactly. The cable does not care if it is charging a phone or a laptop. It only cares that the port fits. It relies on an Interface."

She pointed to his screen. "Look at what you are really doing. os.Open returns a File. http.Get returns a Response Body. strings.NewReader returns a Reader. They are different things, but they all share one behavior: they can read bytes."

"In Go," she continued, "we express this behavior with the io.Reader interface."

The Universal Function

Eleanor took the keyboard. "We replace your three functions with one. We don't ask for a file or a web request. We just ask for 'something that reads'."

// The Universal Function
// We accept io.Reader, the most powerful interface in Go.
func Analyze(r io.Reader) error {
    // We don't know (or care) where the data comes from.
    // We just read it.
    data, err := io.ReadAll(r)
    if err != nil {
        return err
    }
    return processLog(data)
}

Enter fullscreen mode Exit fullscreen mode

"Now," she said, "look how we call it."

func main() {
    // 1. Use a File
    f, _ := os.Open("system.log")
    Analyze(f) // Works!

    // 2. Use a Web Request
    resp, _ := http.Get("http://localhost/logs")
    Analyze(resp.Body) // Works!

    // 3. Use a String
    s := strings.NewReader("ERROR: System failure")
    Analyze(s) // Works!
}

Enter fullscreen mode Exit fullscreen mode

Ethan stared at the main function. "It just... accepts them? I didn't have to tell the File to 'implement' the Reader interface?"

"No," Eleanor said. "That is the beauty of Go. Interfaces are satisfied implicitly. A File has a Read method. The io.Reader interface asks for a Read method. The plug fits, so the current flows."

The Power of Piping

"But wait," Ethan said, looking at the Analyze function again. "I'm still using io.ReadAll. Doesn't that load the entire file into memory? If the log is 10 gigabytes, I'll crash the server."

"You will," Eleanor nodded. "And that is the second benefit of io.Reader. It is a stream."

She deleted io.ReadAll.

"Since r is just a stream of bytes, we can pipe it directly to other streams. Let's say we want to count the lines without ever holding the whole file in RAM."

func Analyze(r io.Reader) error {
    scanner := bufio.NewScanner(r) // Wraps the reader
    count := 0

    // We process line by line as they flow in
    for scanner.Scan() {
        if strings.Contains(scanner.Text(), "ERROR") {
            count++
        }
    }

    fmt.Printf("Found %d errors\n", count)
    return scanner.Err()
}

Enter fullscreen mode Exit fullscreen mode

"This code uses a tiny buffer," Eleanor explained. "You could process a terabyte of logs with this function, and your memory usage would stay flat. You are just connecting pipes."

The Plumbing

Ethan looked at the clean, single function. It was no longer about files or HTTP. It was just about data flowing through a pipe.

"So, io.Reader is like a universal adapter," he said.

"It is the most important abstraction in the language," Eleanor replied, organizing her tapes. "If you write your functions to accept io.Reader, your code becomes compatible with everything: files, networks, buffers, encodings, compressors. You stop building tools that only work in one place, and start building plumbing that works everywhere."

She pushed the cart toward the elevator.

"Stop asking 'what is this thing?' Ethan. Start asking 'what can this thing do?'"


Key Concepts from Chapter 18

io.Reader:
The single most used interface in Go. It defines one method: Read(p []byte) (n int, err error).

  • Concept: "I have data, and you can pull it from me."

Implicit Satisfaction:
You do not declare that a struct implements an interface (like implements Reader in Java). If your struct has a Read method with the right signature, it is a Reader. This allows different packages to work together without knowing about each other.

io.ReadAll vs. Streaming:

  • io.ReadAll(r): Reads the entire stream into memory at once. Easy, but dangerous for large data.
  • Streaming (e.g., bufio.Scanner, json.Decoder, io.Copy): Processes data in small chunks as it arrives. This is memory-efficient and the preferred way to handle io.Reader.

Polymorphism:
The ability to treat different types (File, HTTP Body, String Buffer) as the same type (io.Reader) because they share behavior.


Next chapter: The Interface pollution. Ethan discovers that making interfaces too big is just as bad as not having them at all.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)