I wrote a function that took an interface{} last year because I "wasn't sure yet what would go in it". That sentence should have been the warning. I was sure. I just didn't want to write three types out, so I wrote nothing and let the compiler shrug.
The bill arrives at the call sites. Every place that pulls a value back out needs a type assertion, and every type assertion is a small bet that the thing inside is what you think it is. Miss the comma-ok form and a bad assumption becomes a panic in production rather than an error you handle.
v, ok := x.(string)
if !ok {
return fmt.Errorf("expected string, got %T", x)
}
That's the disciplined version, and it's fine, but I now had that boilerplate in five places, doing the work the type system would have done for free if I'd just named the types. The empty interface didn't remove the decision about what was allowed. It moved it from one place at compile time to five places at runtime.
I'm not against interface{} as such. It's correct for genuinely opaque data passing through, and the standard library uses it well. My regret is reaching for it as a way to defer a decision I'd already made. These days, if I want flexibility, I'd rather define a small interface with the one method I actually call, or just write the union out longhand. More typing today. No 2am type assertion later.