Go's empty interface, interface{}, is the thing you reach for when you want the compiler to stop asking questions. It accepts anything. It is also, in my experience, the single most reliable predictor of a bug I'll be debugging in six months. Every time I've used it to save fifteen minutes, it has come back to charge me an afternoon with interest.
I want to be fair to it first, because the empty interface isn't a mistake in the language. It's the honest representation of "I genuinely do not know the type here". JSON decoding into map[string]interface{}, a logging field that takes anything, the guts of a generic container before generics existed. There are real, correct uses. The trouble is that it's also the path of least resistance, and least resistance is rarely where you want your code to go.
the shape of the regret
The pattern is always the same. I have two or three concrete types that nearly fit a common shape, and writing a proper interface for them feels like ceremony. So I take a shortcut.
func process(items []interface{}) error {
for _, item := range items {
switch v := item.(type) {
case *Order:
// handle order
case *Refund:
// handle refund
default:
return fmt.Errorf("unexpected type %T", v)
}
}
return nil
}
This compiles. It runs. It even looks reasonable. And it has quietly thrown away the one thing I'm paying Go for, which is the compiler telling me at build time when I've passed the wrong thing. That default branch returning an error is the smell. It's a runtime check standing in for a compile-time guarantee I deleted myself. The third type someone adds next year won't get a compile error. It'll get to production and hit the default.
what it actually costs
The cost isn't one big failure. It's a thousand small frictions.
Type assertions everywhere. Once a value is interface{}, every place that uses it has to assert back out, and every assertion is a place that can panic or needs the comma-ok form. The type information existed at the call site and you threw it in the bin, so now every reader has to reconstruct it.
The compiler goes quiet. Rename a field on *Order and the empty-interface paths won't complain. Your IDE can't autocomplete through an interface{}. The refactoring tools that make Go pleasant all stop helping you exactly where you've used it most.
And the documentation evaporates. A function signature is the cheapest, most honest documentation there is. func process(items []interface{}) tells the reader nothing. func process(items []Processable) tells them precisely what's expected, and the compiler enforces that the docs stay true.
what I do instead now
The fix is almost always to spend the fifteen minutes I was trying to save and write the interface.
type Processable interface {
Process(ctx context.Context) error
}
func process(ctx context.Context, items []Processable) error {
for _, item := range items {
if err := item.Process(ctx); err != nil {
return err
}
}
return nil
}
Now *Order and *Refund each carry their own behaviour, the loop doesn't care which is which, and the compiler will refuse to let me pass something that doesn't satisfy the contract. The third type someone adds next year either implements Process or doesn't compile. The check moved from runtime back to build time, which is where I wanted it all along.
When the data really is arbitrary, generics have taken a lot of the remaining pressure off. A type parameter with a sensible constraint expresses "any of these types" without surrendering all type safety, which is exactly the middle ground the empty interface used to occupy badly. []T with T constrained by something meaningful keeps the compiler in the conversation.
My rule now is small and slightly superstitious: if I'm typing interface{} (or any, which is the same thing wearing a nicer jumper), I stop and ask whether I actually don't know the type, or whether I'm just being lazy about declaring it. Nine times in ten it's the second one. The empty interface is for when the type is genuinely unknown, not for when knowing it is mildly inconvenient. I keep relearning that, and I keep writing it down, in the hope that one of these times it finally sticks.