In Go, what does `func Max[T cmp.Ordered](a, b T) T` buy over a version taking `any`?
answer
- same type in, same type out
- no assertion at the call site
- the constraint promises < and >
- an int and a string will not compile
- any loses the caller's type
basics
~10 sA type parameter keeps the caller's concrete type: Max returns T rather than any, so the compiler rejects unorderable or mismatched arguments and the caller never writes a type assertion.
solid answer
~50 s`Max[T cmp.Ordered](a, b T) T` states three things the `any` version cannot. Both arguments and the result are the *same* type, so passing an `int` and a `string` is a compile error rather than a runtime panic. The constraint `cmp.Ordered` promises that `<` and `>` are defined for T, so the body compares directly instead of type-switching over every numeric kind. And the caller gets an `int` back, not an `any` that has to be asserted — the type survives the call. The `any` version must assert internally, panics or returns an error on an unexpected type, and pushes an assertion onto every call site. That is the general rule worth carrying: a type parameter pays when it *links* positions in a signature — argument to argument, argument to result, element to container. Before writing one, check `slices` and `cmp`; the standard library already wrote most of these helpers.
code
go · 15 lines// Pre-generics: the caller gets an any back and must assert.
func MaxAny(a, b any) any {
if a.(int) > b.(int) { // panics unless both are int
return a
}
return b
}
// With a type parameter: checked at compile time, returns T.
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}go deeper
Be ready to write the two-line generic Max and say what the caller gets back. Knowing that the constraint is what makes < legal, and that the any version forces a type assertion, is the whole answer at this level.
Explain what the constraint permits inside the body, and why the any version needs a runtime assertion that can panic. Name cmp.Ordered and the slices package as the standard-library answers rather than hand-rolling.
Show judgment about when not to write it: point at slices and cmp before adding a helper, and describe what an any-typed API costs every caller who must assert on the result.
Frame it as an API commitment. A type parameter and its constraint are part of the exported signature, and adding or tightening one later breaks callers, so decide which of these helpers your codebase should own at all.
## The two signatures ```go func MaxAny(a, b any) any func Max[T cmp.Ordered](a, b T) T ``` `any` is a predeclared alias for `interface{}` — the empty interface, which every type satisfies. A parameter of that type accepts anything, and that is exactly the problem: the function knows nothing about what it received, and neither does the caller about what it gets back. `T` is a *type parameter*. `cmp.Ordered` is its *constraint*: it names the set of types for which `<`, `<=`, `>` and `>=` are defined — the integer kinds, the float kinds, and string. When someone calls `Max(3, 5)`, the compiler works out that T is `int` and compiles the call as though the function had been written for `int`. ## What the caller gains **The type survives the call.** `MaxAny(3, 5)` hands back an `any`. To do arithmetic with it the caller writes `MaxAny(3, 5).(int)`, a type assertion checked at run time; get it wrong and the program panics. `Max(3, 5)` returns an `int`, and the next line can simply add to it. **Mismatches are caught at compile time.** `MaxAny(3, "five")` compiles happily and fails when it runs. `Max(3, "five")` does not compile: T cannot be both `int` and `string`. **Wrong types never enter.** `MaxAny(struct{}{}, struct{}{})` compiles. `Max` with a struct argument does not, because a struct is not in `cmp.Ordered`'s type set. ## What the body gains Inside `MaxAny` you cannot write `a > b`: `>` is not defined for interface values. You have to assert to a concrete type, or type-switch over `int`, `int64`, `float64`, `string` and every other case you want to support — a block of code that is wrong the day someone passes `uint16`. Inside `Max`, `a > b` compiles, once, and works for every type the constraint admits. The constraint is the whole story here. A type parameter constrained by `any` gives the body nothing — not even `==`, which requires the `comparable` constraint. `cmp.Ordered` is what unlocks the comparison operators; a constraint listing a method is what unlocks calling that method. ## The rule this example teaches A type parameter earns its place when it **links two or more positions** in a signature, or when the caller must be able to choose the type that comes back: - argument to argument: two parameters must be the same type; - argument to result: `func First[E any](s []E) (E, bool)` returns the element type of the slice it was given; - element to container: a queue of T yields a T, not an `any`. If a type parameter appears once, as a parameter, with an `any` constraint, it links nothing and buys nothing — the honest signature takes `any`. ## Do not write it if the standard library did Go 1.21 added `slices`, `maps` and `cmp`. Much of what teams hand-wrote in 2022 — `Contains`, `Index`, `SortFunc`, `Max`, `Min` — now lives in `slices`, tested and maintained by the Go team. The first move when someone proposes a generic helper is to look there. ## Where `any` is still right `any` is correct when the function genuinely does not care about the type: it logs it, stores it, or hands it to `encoding/json`. `fmt.Println` takes `...any` for exactly that reason, and no type parameter would improve it — the whole point is a heterogeneous argument list, which a single T forbids. `any` is also right when the caller must be able to mix types in one call. ## What it does not buy A type parameter is not a promise of speed. Go compiles generic code by GC shape with a dictionary, not by generating a fully specialised copy per named type, so a type parameter does not automatically turn an indirect call into a direct one. It buys type safety and readability at the call site; performance is a separate claim requiring a separate measurement.
- When is `any` still the right parameter type in Go?When the function genuinely does not care about the type — it logs it, stores it, or hands it to `encoding/json` or `fmt`. `fmt.Println` takes `...any` for that reason. `any` is also right when the caller must mix several types in one call, which a single type parameter forbids.
- Does a type parameter let the body do more than an `any` parameter does?Only as much as its constraint permits. With `[T any]` you can copy the value and pass it along and nothing else — not even `==`, which needs `comparable`. `cmp.Ordered` is what makes `<` legal; calling a method needs a constraint that names that method.
- Why write your own generic helper when `slices` exists?Usually you should not. `slices.Contains`, `slices.Index`, `slices.SortFunc` and `slices.Max` cover most element-wise work since Go 1.21, and they are tested and maintained for you. The first question about any proposed generic helper is what the standard library already provides.
The any version is a parcel with no label: the courier will carry anything and the recipient has to open it to find out what arrived. The generic version is a labelled shipping lane — only orderable goods enter, and what comes out the far end is what went in.
saying these in an interview costs you the question
- Says the any version is faster because it skips generics
- Thinks a type assertion on an any value is checked at compile time
- Believes any and interface{} are different types
- Reaches for a type parameter before looking at the slices package
- Claims a type parameter constrained by any supports == or <