skip to content

In Go, what does the `[T any]` in `func Reverse[T any](s []T)` declare, and where can T be used?

level: juniorimportance: must knowfreq 70%

answer

  1. brackets sit before the parentheses
  2. a name plus a constraint
  3. T is a type, not a value
  4. in scope for signature and body
  5. any promises no operations

basics

~20 s

The brackets declare a type parameter list. T is a placeholder for a type, constrained by any, so one Reverse serves every element type. T is in scope for the rest of the signature and the whole body.

solid answer

~40 s

The square brackets between the function name and the parameter list declare **type parameters**. `[T any]` introduces one type parameter named `T` whose constraint is `any`, meaning any type at all may be substituted for it. `T` is then a genuine type name: it is in scope for the rest of the signature (`s []T`, and any result type) and for the entire function body, so I can write `make([]T, len(s))` or `var tmp T` inside. Each call substitutes a concrete type — `Reverse([]int{...})` uses `T = int`, `Reverse([]string{...})` uses `T = string` — and the compiler type-checks the body once against the constraint. Because the constraint is `any`, the body may only move values of type `T` around; it cannot compare them with `==` or add them, since `any` promises no operations.

code

go · 9 lines
go
func Reverse[T any](s []T) {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
}

// Reverse([]int{1, 2, 3})       instantiates with T = int
// Reverse([]string{"a", "b"})   instantiates with T = string
// rev := Reverse[string]        an instantiated function value

go deeper

for a junior

Be ready to write the declaration from memory: brackets after the name, each entry a name plus a constraint, and any as the permit-everything constraint. Say out loud that T is a type, not a value.

for a middle

Explain scope precisely — T covers the rest of the signature and the whole body — and why a T constrained by any cannot be compared or added. Contrast it with an old []interface{} parameter.

for a senior

An interviewer expects you to judge when a type parameter earns its place versus a plain parameter, and to explain the stenciling-plus-dictionaries implementation well enough to say why generics are not free and not erased.

for a principal

Own the guidance your codebase gives: which helpers are worth making generic, how many type parameters a signature may carry before it stops reading, and what you tell reviewers to push back on.

## The shape of the declaration A Go generic function puts a **type parameter list** in square brackets between the function name and the ordinary parameter list: ```go func Reverse[T any](s []T) // ^^^^^^^ type parameter list // ^^^^^ ordinary parameter list ``` Every entry in that list is a *name* followed by a *constraint*: `T` is the name, `any` is the constraint. The constraint is mandatory — there is no bare `[T]` — and `any` (an alias for `interface{}` added as a predeclared name in Go 1.18) is the constraint that permits every type. The position matters. The brackets go **after the function name and before the parentheses**. `func [T any] Reverse(...)` and `func Reverse(s []T) [T any]` are both syntax errors, and `Reverse<T>(...)` is another language's spelling. ## What T actually is `T` is not a value and not a variable. It is a **type name** that stands for whatever type the call site supplies. Inside the function it behaves like any other type name: ```go func Reverse[T any](s []T) { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } } ``` You can declare variables of type `T`, build `[]T`, `map[string]T`, `chan T` or `*T`, pass `T` values to other generic functions, and name `T` in the result type. The one thing that changes relative to a normal type is that the compiler checks the body against the **constraint**, not against any particular concrete type. With `any` as the constraint, the set of operations available on a `T` value is deliberately tiny: assignment, passing, storing, taking its address. `a == b`, `a + b`, `a < b`, indexing and calling are all rejected, because `any` does not promise that every possible substitution supports them. ## Scope A type parameter is in scope from its declaration in the bracket list through: - the constraints of the other type parameters in the same list, - the ordinary parameter types and the result types, - the entire function body. It is *not* in scope outside the function, so two different functions may each declare a `T` and they are unrelated. Nor does it exist at run time as something you can print or switch on directly; it is a compile-time notion that gets resolved before the program runs. ## Instantiation Writing the function is only half of it. A generic function becomes callable when it is **instantiated** — the compiler substitutes a concrete type argument for `T`. In the common case the type argument comes from the call arguments, so `Reverse(nums)` reads exactly like a call to a non-generic function; you may also write it out, as `Reverse[int](nums)`. Every distinct instantiation is conceptually its own function, and `Reverse[int]` may even be assigned to a variable of function type. An instantiated generic function is a normal function value: ```go rev := Reverse[string] // type: func([]string) rev([]string{"a", "b"}) ``` ## What the compiler emits A frequent interview follow-up is whether Go produces one machine-code copy per type argument. It does not do full monomorphisation, and it does not erase types either. Go's implementation uses **GC-shape stenciling with dictionaries**: types that have the same shape from the garbage collector's point of view (all pointer-shaped types, for instance) share one compiled body, and a hidden dictionary argument carries the per-instantiation details the body needs. The practical consequence for a candidate is simply that generics are neither free in the way erasure is nor as aggressively specialised as C++ templates — but the semantics you reason about are the substitution semantics above. ## Why this beats `interface{}` Before Go 1.18 the only way to write one `Reverse` was `func Reverse(s []interface{})`, which forced callers to copy their slice element by element into a `[]interface{}` and lost the element type on the way out. The type parameter keeps the element type: `Reverse` applied to a `[]int` still deals in `int`, with no boxing and no type assertions, and the compiler rejects a mixed-type call at build time rather than at run time. ## Common mistakes - Putting the bracket list in the wrong place. - Assuming `any` lets the body use `==` or `<`. - Thinking `T` is only usable in the parameter list — it is usable in results and in the body too. - Declaring a type parameter that appears exactly once and is never constrained, which usually means an ordinary `any` parameter would have been simpler.

  • Can you declare local variables of type T inside the function body?
    Yes. `T` is a type name for the whole body, so `var tmp T`, `make([]T, len(s))`, `map[string]T` and `*T` are all legal. What is restricted is what you may *do* with a `T` value: under the constraint `any` you can copy and pass it, but not compare it with `==`, add it or index it.
  • Does the compiler emit one copy of the function per type argument?
    No — Go uses GC-shape stenciling plus dictionaries. Instantiations whose type arguments have the same garbage-collector shape share one compiled body and receive a hidden dictionary with the per-instantiation details. It is neither full monomorphisation nor type erasure.
  • If two functions in the same package each declare `[T any]`, are the two T's related?
    No. A type parameter's scope is its own function, so each declaration introduces a fresh, unrelated name. The identifier being spelled the same is a readability convention, nothing more; one function's `T` can be instantiated with `int` while the other's is `string` in the same expression.

saying these in an interview costs you the question

  • Calls T a variable that holds a value at run time
  • Puts the type parameter list after the ordinary parameter list
  • Thinks a T constrained by any supports == or +
  • Says T may appear only in the parameters, not in results or the body
  • Claims Go erases type parameters like a boxed interface{} parameter