In Go, why is `func Handle[T any](v T)` no more capable than `func Handle(v any)`?
answer
- count the positions T appears in
- the constraint decides what the body may do
- an any constraint permits no operation
- not even == without comparable
- one position ties nothing to anything
basics
~20 sA type parameter constrained by any permits no operations — not even ==. With T appearing in only one position it links nothing to nothing, so the generic form does exactly what the plain any parameter does, with more syntax.
solid answer
~50 sTwo things make that signature empty. The constraint is `any`, so inside the body T supports nothing: no `==` (that needs `comparable`), no operators, no methods — you can copy the value and pass it on. And T appears once, as a parameter type, so it links nothing: no other parameter must match it and no result carries it back. Both together make the type parameter decoration; `Handle(v any)` is the honest signature. The rule's positive form: a type parameter pays when it *ties positions together* — `func First[E any](s []E) (E, bool)` hands the caller a `Row` back from a `[]Row` with no assertion — or when the caller picks the type that comes out, as in `func New[T any]() *T`. One difference is real but small: the generic form passes the value as its own type instead of converting it to an interface, which can avoid an allocation.
code
go · 14 lines// T is used once and constrained by any, so it buys nothing.
func HandleGeneric[T any](v T) { log.Printf("%v", v) }
// Same capability, less syntax.
func Handle(v any) { log.Printf("%v", v) }
// Earns it: E ties the element type to the result type.
func First[E any](s []E) (E, bool) {
if len(s) == 0 {
var zero E
return zero, false
}
return s[0], true
}go deeper
Be ready to say what a constraint of any allows inside the function: copying and passing the value, and nothing else. Recognising that a lone type parameter adds no safety is enough here.
Explain both tests out loud — what the constraint permits, and how many positions the type parameter occupies — and give one signature that passes, such as returning an element type from a slice.
Turn it into a review rule others can apply without you, and know the one real difference: the generic form avoids converting the value to an interface, which is an allocation claim you verify rather than assert.
Own the consequence for exported APIs. Once a type parameter is published, removing it changes every call site, so the cost of an empty one is paid by every team that imported the package.
## Two independent tests, and this signature fails both ### Test one: what does the constraint permit? A type parameter is only as useful as its constraint. Constrained by `any`, T promises nothing about the values it stands for. Inside the function you may: - copy the value, assign it, pass it to another function that takes T or `any`; - take its address, put it in a slice or a map value. You may **not**: - compare with `==` or `!=` — that requires the `comparable` constraint; - use `<`, `+` or any other operator — that requires a constraint whose type set defines them; - call a method — that requires a constraint naming the method; - type-switch on it directly. `v.(type)` is only legal on an interface value, so you must write `switch x := any(v).(type)`, converting to an interface first. That last one is the tell. If the body's first move is `any(v)`, the type parameter has already been discarded, and the parameter should have been `any` from the start. ### Test two: how many positions does T appear in? A type parameter is a *relationship*. Its value is that two or more places in the signature are forced to agree: - `func Equal[T comparable](a, b T) bool` — the two arguments must be the same type. - `func First[E any](s []E) (E, bool)` — the result type follows from the argument's element type. - `func Map[E, R any](in []E, f func(E) R) []R` — three positions locked together. `func Handle[T any](v T)` has T in one place. Nothing is being forced to agree with anything, so nothing is gained. The one honest single-occurrence case is a type parameter in the **result**, chosen by the caller: `func New[T any]() *T`, or a decoder that returns the type you ask for. There the type parameter is not inferable and every call site writes it out — a real cost — but it does something a non-generic signature cannot: return the caller's chosen type without an assertion. ## Is there any difference at run time? Yes, in representation, not capability. `Handle(v any)` converts the argument into an interface value; for a non-pointer type that conversion may put the value on the heap. `Handle[T any](v T)` passes the value as its own type, so it can stay in the frame. If the function is hot and takes small values, that is a genuine allocation argument — but it is an argument you make with a benchmark, not with a signature. There is also a cost on the other side. Every instantiation is compiled work, and generic code carries a dictionary; a type parameter that buys nothing is not free. ## Why this shows up so often After Go 1.18 the reflex "generic is the modern way to accept anything" is common, and it produces exactly this signature. It survives review because it looks type-safe. It is not: `[T any]` gives the compiler no more information about what the body may do than `any` does — the difference is only that with `any` the loss of type information is visible in the signature, which is the honest thing to show a reader. ## The rewrite Three possible fixes, in order of preference: 1. **A concrete type.** Most functions that were made generic to "accept anything" only ever get one or two types. `func Handle(v Event)` is the best signature of all. 2. **A small interface.** If the body needs behaviour — writing, closing, stringifying — take the interface that names it: `func Handle(w io.Writer)`. That is Go's oldest and most idiomatic answer, and it lets the caller pass a mock, a wrapper or a buffer. 3. **`any`.** If the body genuinely just stores, logs or serialises the value, `any` says so honestly. A type parameter is the answer only when a relationship between types must be preserved through the call. ## Reviewing for it The check takes seconds: read the type-parameter list, then count occurrences of each parameter in the signature. One occurrence in a parameter position with an `any` constraint is a delete. It is one of the few generics review rules that needs no judgment at all.
- Is there any real difference between the two signatures at run time?In representation, not capability. `Handle(v any)` converts the argument to an interface value, which for a non-pointer type may heap-allocate; `Handle[T any](v T)` passes the value as its own type and it can stay in the frame. That is an allocation argument to be measured, not a reason to prefer the shape by default.
- How would you type-switch on the value inside `Handle[T any](v T)`?You convert first: `switch x := any(v).(type)`. A type-parameter value is not an interface value, so it has no dynamic type to switch on directly. Needing that conversion is a reliable sign the type parameter is doing no work and the parameter should simply be `any`.
- What makes `func First[E any](s []E) (E, bool)` different?E appears twice and ties the two together: the element type of the argument determines the result type, so a `[]Row` yields a `Row` with no assertion at the call site. Preserving that relationship through the call is precisely what a type parameter is for.
- Is a type parameter that appears only in the result always a mistake?No. `func New[T any]() *T` uses T once, but the caller chooses it and gets that exact type back, which no non-generic signature can do. The price is that inference cannot help, so every call site writes the type argument — worth it when the returned type genuinely varies, wasteful otherwise.
saying these in an interview costs you the question
- Says [T any] is type-safe while any is not, without saying what T permits
- Adds a type parameter so the signature looks like modern Go
- Thinks a T constrained by any supports == or <
- Cannot point to a second position where T is used
- Writes any(v) inside the body and keeps the type parameter anyway