Ramblings of an aging IT geek
← Ramblings of an aging IT geek
golang

every interface{} i wrote came back to bill me

Why I stopped reaching for interface{} as a convenience in Go, the runtime type assertions it scattered through the code, and what generics let me delete.

A monitor showing Go source code

interface{} is Go's way of saying "I'll work out what this is later", and the trouble is that later always arrives, usually in production, usually at the type assertion you forgot to guard. I spent years scattering empty interfaces through code because they were convenient at the call site, and every one of them was a small debt that came due somewhere downstream.

The pattern is seductive. You've got a function that handles a few different shapes, you don't fancy writing it three times, so the parameter becomes interface{} and the body becomes a type switch. Done. Except the compiler now knows nothing about what flows through that function, so every place that pulls a concrete value back out is a runtime assertion that can panic, and the only documentation of what's actually allowed in there is the type switch itself, buried in the body.

A close-up of code with a type switch on a screen

I had a small library doing exactly this for a config-merging helper. Values came in as interface{}, got switched on, got asserted back out. It worked. It also had a comment that said // add new types here too next to the type switch, which is the kind of comment you write the day you realise the abstraction has a memory and you don't.

Generics fixed most of it, and I came to them grudgingly because I'd spent years arguing Go didn't need them. The merging helper became func Merge[T any](a, b T) T and the type switch evaporated. The compiler now knows what T is at every call site, the assertions are gone, the panic-on-bad-type case is gone because the bad type doesn't compile, and the // add new types here too comment is gone because there's nothing to keep in sync. Three lines of code I get to delete and one whole class of bug I get to stop worrying about.

I'm not on a crusade. There are still legitimate uses for interface{}, now spelled any, mostly at genuine boundaries where you really are handed something of unknown shape and your job is to inspect it. JSON decoding into an unknown structure, for one. But "I didn't want to write it twice" is not that boundary, it's just laziness wearing a type system's clothes, and the bill always finds you. These days, before I reach for any, I make myself answer one question: do I genuinely not know the type here, or do I just not want to name it? Almost always it's the second, and almost always there's a type parameter that does the job and lets the compiler do the worrying.