---
title: "type erasure in go? how io.reader undermines everything you know about interfaces"
author: "pilput"
canonical: "https://pilput.net/pilput/type-erasure-in-go-how-ioreader-undermines-everything-you-know-about-interfaces"
published: "2025-09-06T04:13:02.010528Z"
updated: "2025-09-06T04:13:02.010528Z"
description: "what is type erasure, anyway? before we dive deep into go's magical and sometimes confusing world of interfaces, let's understand a core concept: type erasure ..."
---
# type erasure in go? how io.reader undermines everything you know about interfaces

## what is type erasure, anyway?

before we dive deep into go's **magical and sometimes confusing** world of interfaces, let's understand a core concept: **type erasure**.

in some programming languages (like java), generics are implemented using type erasure. this means the compiler removes all information about a generic type (like `list`) after checking it for correctness. at runtime, the program only sees a raw `list`, and the fact that it was supposed to hold strings is "erased."

this is a key background concept because go's interfaces achieve a similar kind of "hiding" of the underlying type, but they do it **without true generics** (at least until recently) and in a uniquely go way.

## a quick go interfaces refresher

in go, an interface is a set of method signatures. a type implements an interface simply by having methods that match those signatures. there's no explicit `implements` keyword.

this is often taught with simple examples:

```
type speaker interface {
    speak() string
}

type dog struct { name string }

func (d dog) speak() string {
    return "woof! my name is " + d.name
}

type cat struct { name string }

func (c cat) speak() string {
    return "meow! i'm " + c.name
}

func makenoise(s speaker) {
    fmt.println(s.speak())
}

func main() {
    dog := dog{"rex"}
    cat := cat{"whiskers"}
    makenoise(dog) // works! dog implements speaker.
    makenoise(cat) // works! cat implements speaker.
}
```

this is the "everything you know about interfaces" from the title. it's clean, intuitive, and forms the bedrock of go's polymorphism. the function `makenoise` accepts any concrete type (`dog`, `cat`) that fulfills the `speaker` contract.

## introducing the magical io.reader

now, let's meet `io.reader`, perhaps the most famous interface in the go standard library and a cornerstone for **devops tools, web servers, and data processing** (key for any **full stack** developer).

its definition is beautifully simple:

```
type reader interface {
    read(p []byte) (n int, err error)
}
```

anything that has a `read` method with this exact signature is an `io.reader`. this includes files (`os.file`), network connections (`net.conn`), strings (`strings.reader`), http request bodies, and bytes in memory (`bytes.buffer`).

the power here is **abstraction**. you can write a function that accepts an `io.reader` without caring if the data is coming from a file on disk, a network socket, or a user typing on a keyboard.

```
func processinput(r io.reader) error {
    data, err := io.readall(r)
    if err != nil {
        return err
    }
    fmt.printf("processing: %s\n", data)
    return nil
}
```

## how io.reader "undermines" the classic view

so, how does this simple interface "undermine everything"? it introduces a different, more powerful pattern.

in our `speaker` example, the interface was about **what an object is** ("i am a speaker"). with `io.reader`, the interface is about **what an object can do** ("i can give you bytes"). this is a subtle but profound shift in thinking.

`io.reader` often doesn't care about the underlying data structure _at all_. it's not about passing a complete object; it's about granting **access to a stream of data**. the concrete type behind the interface is often completely irrelevant to the function consuming it. this is a form of type erasure—the receiving function only sees the capability to `read`, not the identity of the type providing it.

### the empty interface: interface{}

this leads us to the ultimate erasure: the empty interface, `interface{}`. an interface with zero method requirements is implemented by _every_ type. it literally erases all type information.

while `io.reader` is a focused, well-defined contract, `interface{}` is a wildcard. it's used when you need to accept any possible type, like in functions such as `fmt.println`. use it sparingly, as you lose the safety of go's type system.

```
func takeanything(i interface{}) {
    // i can be a string, int, dog, cat, reader... anything!
    fmt.printf("type: %t, value: %v\n", i, i)
}
```

## why this powers the go ecosystem

this design philosophy is not a flaw; it's go's superpower for **coding** robust systems.

- **decoupling:** code depends on behavior, not concrete implementations. this makes your code more modular and testable. you can easily mock an `io.reader` in tests.

- **composition:** small, focused interfaces (like `reader` and `writer`) can be composed into more powerful abstractions.

- **simplicity:** the standard library can provide universal functions like `io.copy(dst writer, src reader)` that work across files, networks, and buffers seamlessly.

for **seo** and web development, think about parsing http requests. the `http.request.body` is an `io.reader`. your json or form parsing logic doesn't care if the body came from a gzip-compressed stream, a direct tcp connection, or a unit test. it just reads from the provided `reader`.

## conclusion: embrace the erasure

don't be afraid that `io.reader` "undermines" your understanding. it actually **elevates it**. it shows you the next level of go's interface power: moving from modeling objects to modeling behaviors and capabilities.

the next time you use an `io.reader`, remember—you're not just using an interface; you're wielding a fundamental abstraction that erases unnecessary details and focuses purely on the flow of data, a concept that is vital for effective **coding** in go.
