Why does a Go function that returns `any` push work onto every caller, and how does a type parameter remove it?
answer
- the caller pays for the missing type
- what does v.(bool) do when it is not
- the compiler cannot see inside any
- T flows out to the call site
basics
~20 sReturning any gives the caller a value with no static type, so every call site must type-assert and can panic at run time. A type parameter returns the caller's own type instead, so the compiler checks the use.
solid answer
~40 sA function returning `any` has thrown the type away, so each caller writes `v, ok := x.(bool)` and has to decide what to do when the assertion fails — boilerplate at every call site, and a panic if anyone forgets the comma-ok form. Making the function generic, `func Lookup[T any](c *Client, name string) (T, error)`, moves that decision into the signature: `Lookup[bool](c, "dark-mode")` is statically a `bool`, a caller who uses it as a string gets a compile error, and the SDK still owns the one runtime check that the stored value really is a bool. The point is not speed, it is where the type mismatch is caught: at compile time in one place, instead of at run time in every caller.
code
go · 8 linesv, err := Lookup(c, "dark-mode") // v is any
if err != nil {
return err
}
on, ok := v.(bool)
if !ok {
return fmt.Errorf("dark-mode is %T, not bool", v)
}go deeper
Be ready to show both call sites side by side: the assertion with comma-ok on the any version, and the plain typed result on the generic one. Know that the one-result assertion panics.
Explain where the failure moves rather than disappears: one runtime check inside the package versus an assertion in every caller, and why var zero T is how a generic body produces a zero value.
Show judgment about which boundaries deserve a type parameter at all. Unknown-shape data still wants any, and a generic wrapper over an any-typed core is often the cheapest way to offer both.
Own the consequence for consumers: a package-level generic helper is additive and removable, while turning the core API generic commits every importer's source. Say which one you would ship first.
## What `any` actually gives the caller `any` is a predeclared alias for `interface{}`, added in Go 1.18 as a spelling convenience. A value of type `any` carries a dynamic (type, value) pair at run time, but statically it is opaque: the compiler knows nothing about it, so you cannot call methods on it, compare it usefully, or assign it to a `bool` variable. To get back a usable value you write a **type assertion**: ```go on := v.(bool) // panics if v does not hold a bool on, ok := v.(bool) // comma-ok form: ok is false instead of panicking ``` The one-result form **panics** when the dynamic type is wrong. That is the trap: a package that returns `any` has silently delegated a runtime failure mode to everyone who imports it. ## Why pre-generics Go looked like this Before Go 1.18 there were no type parameters, so a container or lookup that had to work for several element types had exactly one option: `interface{}` in, `interface{}` out, plus assertions at the boundary. A feature-flag SDK written that way exposes something like `Lookup(c *Client, name string) (any, error)`, and every consumer team repeats the same six lines: ```go v, err := Lookup(c, "dark-mode") if err != nil { return err } on, ok := v.(bool) if !ok { return fmt.Errorf("dark-mode is %T, not bool", v) } ``` Multiply that by a few hundred flag reads across a dozen services and the cost is real: it is duplicated, it is easy to get wrong (`v.(bool)` without `ok`), and the mistake shows up in production rather than in `go build`. ## What the type parameter changes ```go func Lookup[T any](c *Client, name string) (T, error) { var zero T // ... return zero, nil } ``` Now `T` flows out of the function into the caller's expression. `Lookup[bool](c, "dark-mode")` has static type `bool`; assigning it to a `string` is a compile error; `if on { ... }` just works with no assertion. `var zero T` is how a generic function produces the zero value of an as-yet-unknown type, since it cannot write `nil`, `0` or `false`. Note what did **not** change. A flag store holds whatever was configured; if operations set `dark-mode` to a number, something must still notice at run time. The difference is that the check now lives **once**, inside the SDK, which can return a good error naming the flag and both types — instead of living at every call site as an assertion that a tired reviewer will not question. ## Where the type parameter must go It is a package-level function taking the client, not a method on it. Methods could not declare their own type parameters until Go 1.27, and even now a generic method cannot implement an interface method, so `func (c *Client) Lookup[T any](...)` is the shape to avoid in a widely imported package. `func Lookup[T any](c *Client, ...)` works on every supported Go version. ## When `any` is still the right answer Generics do not abolish `any`. Use `any` when the data genuinely has no single static type at that boundary: - decoding JSON of unknown shape into `map[string]any`; - a logging or formatting helper that prints whatever it is handed (`fmt.Println` takes `...any` for exactly this reason); - a value the package stores and hands back untouched, where the caller — not the package — knows the type. The useful test is: *does the caller already know the type at the call site?* If yes, a type parameter carries that knowledge into the compiler. If no, `any` is honest and a type parameter would just be a wordier `any`. ## The trap to avoid A constraint of `any` on a parameter that the body only stores and returns is fine. A constraint of `any` on a body that wants to add, compare or print `T` is not: inside `func F[T any](v T)` you may only do what *every* type permits — assign it, pass it, take its address, put it in a slice. `v + v`, `v < w` and `v.String()` are all compile errors until the constraint says otherwise. That is the subject of the next question up the ladder.
- Does the generic version stop a caller asking for the wrong type entirely?It fixes the static type, not the stored one. A flag configured as a number is still wrong at run time, so the implementation keeps one check and returns an error naming the flag and both types. The gain is that the check lives once inside the package instead of at every call site.
- When is returning `any` still the right design?When the caller does not know the type either: decoding JSON of unknown shape into `map[string]any`, or a print helper taking `...any`. A type parameter that the caller can only instantiate as `any` is just a wordier `any`.
- Why write `Lookup[T any](c *Client, ...)` rather than a method on `*Client`?Methods could not declare their own type parameters before Go 1.27, and even now a generic method cannot implement an interface method. A package-level generic function taking the client compiles everywhere and keeps the client usable through interfaces.
Returning any is like handing over an unlabelled box: every recipient has to open it and hope. A type parameter is a label the recipient chose, and the compiler checks the label.
saying these in an interview costs you the question
- Says a failed type assertion returns the zero value instead of panicking
- Claims generics remove all runtime type checking
- Thinks any and a type parameter are interchangeable spellings
- Believes Go had type parameters before 1.18
- Reaches for any to avoid writing the constraint the code actually needs