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

interface{}, and the regrets it left me

How an innocent interface{} in a config loader spread through a Go codebase until type assertions were everywhere, and what I'd do differently.

A Go source file open in an editor

It started, as these things do, with one small interface{}. A config loader needed to hold "a value", and the value could be a string, a number, or a bool. The empty interface holds anything, so I reached for it. Done in a minute. Felt clever.

interface{} is Go's way of saying "I give up on knowing the type". Which is fine, occasionally, at a genuine boundary where you really don't know. The trouble is that it's contagious. Once a value is interface{}, everything that touches it has to either accept interface{} too, or do a type assertion to claw the real type back out. So my one honest unknown at the edge of the program quietly became dozens of type assertions scattered through code that absolutely did know what it was dealing with.

Go code showing repeated type assertions

The assertions are the regret. Every one of them is a little runtime gamble that the compiler can't help you with:

v, ok := cfg["timeout"].(int)
if !ok {
    return fmt.Errorf("timeout was not an int")
}

Multiply that by every field in the config and you've reimplemented type checking by hand, at runtime, badly, in a language that has a perfectly good type system you chose to switch off. And the failure mode is the worst kind: it compiles fine, ships fine, and falls over in production the first time someone writes timeout: "30s" and the assertion to int returns ok = false. The compiler knew nothing because you asked it to know nothing.

What I should have done is define the shape. A real struct, with real fields and real types, and let the unmarshalling layer do the conversion once, at the boundary, where the unknown actually lives.

type Config struct {
    Timeout int    `json:"timeout"`
    Host    string `json:"host"`
    Debug   bool   `json:"debug"`
}

Now the timeout is an int everywhere downstream. No assertions. No ok checks scattered through business logic. If the JSON is wrong, it fails once, at unmarshal time, with a clear error, in the one place that's allowed to not know the type yet. Everywhere else gets to be honest about what it's holding.

The lesson I keep relearning: interface{} isn't a tool for holding data, it's a tool for losing type information. Sometimes you genuinely want to, at a true boundary. But reach for it out of laziness and you don't avoid the work of describing your types. You just defer it, scatter it, and convert it from a compile error into a 3am page. Describe the shape up front. Pay the small cost once, at the edge. Future-you, holding the pager, will be grateful.