Go does not have generics, and interface{} is the door everyone walks through when they hit that wall. I walked through it more than once, and I regret most of those trips. The empty interface satisfies everything, which sounds like a feature until you realise it means the compiler can no longer help you with anything.
The case that stings most was a small event-processing pipeline. I had three or four message types and a handful of stages that passed them along. Rather than write the plumbing per type, I made the channels chan interface{} and felt clever for about a week. Then the type assertions arrived.
func (s *stage) process(msg interface{}) {
switch m := msg.(type) {
case OrderEvent:
s.handleOrder(m)
case RefundEvent:
s.handleRefund(m)
default:
// and here is the problem
log.Printf("unhandled message type: %T", msg)
}
}
That default branch is the whole story. With concrete types, forgetting to handle a new message is a compile error. With interface{}, it is a log line at runtime, on a Tuesday, in production, if you are lucky enough to be looking. I added a fifth event type months later, wired it into the producer, and the pipeline silently dropped it on the floor because I never touched the type switch. No error. No warning. Just events that quietly went nowhere until someone asked why a report was short.
The deeper cost was not the bugs, it was what the code stopped telling me. A function signed func(OrderEvent) documents itself. A function signed func(interface{}) documents nothing. Every reader, including me six months later, has to go and find the type switch to learn what the thing actually accepts. I had traded a little typing now for a permanent tax on comprehension later, paid by everyone who reads the code, forever.
What I do now is boring and I am happier for it. If there are genuinely a fixed, known set of types, I use a real interface with a real method set, so the compiler enforces that every type implements what the pipeline needs and a new type cannot sneak through unhandled. If the set is open and I truly cannot know the types ahead of time, then interface{} is the right tool and I accept the assertions as the cost of the flexibility I actually wanted. The mistake was never the empty interface itself. It was reaching for it to dodge a bit of duplication, and calling the result generic when it was only vague.
There is talk of generics finally landing in the language, and the proposal that is circulating looks like it would solve exactly this class of problem properly. I am cautiously hopeful. Until then I will keep choosing the slightly longer, slightly more repetitive code that lets the compiler do its job, because the compiler is the one teammate who never gets tired and never forgets to check.