Every time I have reached for interface{} in Go to be clever, I have paid for it later. Not always immediately, which is the problem. It works fine in the demo and the tests, then fails in production with a panic that points at a type assertion buried three layers down.
The seduction is always the same. You have two or three functions that do nearly the same thing to different types, and the duplication itches. So you write one function that takes interface{}, switch on the type inside, and feel briefly pleased. What you have actually done is take a problem the compiler was solving for you and move it to runtime, where you solve it instead, badly.
func process(v interface{}) (string, error) {
switch x := v.(type) {
case User:
return x.Name, nil
case Account:
return x.Owner, nil
default:
return "", fmt.Errorf("unsupported type %T", v)
}
}
Look at that default. That is a whole category of bug I have just invented. The compiler used to guarantee I only called this with things it could handle. Now I get a runtime error for a mistake that, with a real type, would never have built. Every new type that should be supported is a silent gap until someone hits it.
The honest version is usually less clever and much better. If the types genuinely share behaviour, define an interface around that behaviour, the small specific kind, and let each type implement it:
type Named interface {
DisplayName() string
}
func process(v Named) string {
return v.DisplayName()
}
Now the compiler is back on my side. Pass it something that does not implement Named and the build fails on my machine, today, with a clear message, instead of in production at the worst possible moment. The duplication I was trying to remove was three lines. The bugs I introduced removing it cost me considerably more than three lines to find.
There are real uses for interface{}: the boundary where you genuinely do not know the type, decoding arbitrary JSON, a logging call that takes anything. The mistake is using it for types I do know, inside my own code, to save a little typing. These days when I feel the itch to reach for it, I treat that as a signal that I have not yet worked out what the types actually have in common. Once I do, the right interface is small, specific, and the compiler does the work I was about to do by hand. Generics will eventually take the edge off this, but the rule outlives the workaround: do not throw away type information you have, only to reconstruct it at runtime where it can fail.