interface{} is Go's way of saying "I give up on knowing what this is". I used to reach for it whenever a type got awkward, and nearly every time, I regretted it later. Not immediately. That's the trap. It works beautifully right up until it doesn't, and by then the cost is spread across a dozen call sites.
The appeal is obvious. You've got a function that could take a string or an int or a struct, and writing it generically with interface{} makes the awkwardness disappear. The compiler stops complaining. You feel productive. What you've actually done is move the type check from compile time, where it's free and certain, to runtime, where it's a panic waiting for the right input.
Here's the shape of the regret. You write something like this, store a value, and pull it back out later:
func (c *Cache) Get(key string) interface{} {
return c.items[key]
}
// somewhere far away, written by someone who isn't you
count := c.Get("retry_count").(int)
That type assertion is a landmine. The day someone stores a retry count as an int64 instead of an int, or the key doesn't exist and you get a nil, this panics in production rather than failing in CI. The compiler had all the information it needed to stop you and you specifically told it not to look.
The other cost is that interface{} is contagious. Once a value is "any", everything that touches it has to assert, or pass the "any" along and let the next person assert. The type information you threw away doesn't come back. Documentation gets vaguer. The function signature, which should be the truest comment in the codebase, now says nothing at all. func Process(data interface{}) interface{} tells a reader precisely nothing about what goes in or comes out.
What I do now, in order of preference:
- Use a concrete type. Most of the time the "I need this to be generic" feeling is wrong, and the function only ever gets called with one type anyway. Write that type.
- Use a small interface that describes behaviour. If you genuinely need polymorphism, an
interface { Read(p []byte) (int, error) }is a contract the compiler enforces.interface{}is the absence of a contract. - If you must use
interface{}, contain it. Keep it at the very edge, decode it into a real type immediately, and never let an "any" wander into your business logic. The boundary of a JSON decoder is a fair use. The middle of a domain model is not.
None of this is news to anyone who's read the Go proverbs. "interface{} says nothing" is right there. But there's a difference between reading it and having spent an afternoon chasing a panic that turned out to be a type assertion on a value that used to be an int and quietly became a float64 after a JSON round-trip. I've had that afternoon. The proverb earns its keep.
We'll probably get real generics in the language eventually, and a lot of the legitimate uses for interface{} will evaporate when we do. Until then, my rule is simple: every time I reach for the empty interface, I make myself say out loud what type I'm actually throwing away. Usually that's enough to talk me out of it.