Type parameters are the placeholders inside square brackets that turn a regular function or type into a generic one. The syntax [T any] declares a parameter named T that the compiler fills in with a real type at each call site. This chapter is a close look at the syntax itself: where the brackets go, how to name parameters, how to declare multiple of them, how scope works, and the two predeclared constraints (any and comparable) you can use without writing your own.
A type parameter lives between the function name and the regular parameter list, inside square brackets. The simplest possible generic function has one type parameter and uses it once.
Three things are happening in that signature. The name T is a new identifier the compiler creates for the duration of the function. The word any is the constraint, the set of types T is allowed to be. And the parameter list and return type can both refer to T, which makes the function shape-agnostic: whatever type the caller passes is the type it gets back.
Here's the same anatomy in a diagram. Each piece of the bracket header plays a specific role:
The orange block is the only piece that's new compared to a regular function. Everything to its right is the same syntax you already know: a parenthesized parameter list, then a return type. The type parameter block can declare one or many parameters, each with its own constraint, and any of those names can appear in the regular parameter list, the return type, the function body, or all three.
Let's swap Identity for something an e-commerce app would actually use. A First function returns the first element of a slice, regardless of what's in the slice:
The function is written once but works for any slice element type. At the first call, the compiler picks T = string because products is []string. At the second call, it picks T = float64. There is no runtime cost to this dispatch in the way you might expect from boxing or reflection; the Go compiler generates code that uses T directly.
The constraint any deserves a quick word. It means "no restriction on what T can be." That's the right choice for First, because reading the first element doesn't require any operation that's specific to a particular type. As soon as you want to compare values, add them, or call methods on them, you need a tighter constraint.
The position of the type parameter block depends on whether you're declaring a function or a type. The rule is simple, but the two cases look different at a glance.
For a function, the type parameters go between the function name and the regular parameter list:
For a type (struct, alias, or named type), the type parameters go between the type name and the underlying type definition:
For a method on a generic type, the type parameters live on the receiver, not on the method name. You re-state the type parameter on the receiver, and the method doesn't introduce new ones:
Notice (c *Cart[T]) on the receiver. The [T] after Cart is mandatory: it tells the compiler which type parameter from the type declaration this method binds to. The receiver re-introduces T into the method body's scope. You cannot put new type parameters on the method itself, only inherit them from the receiver. That's a deliberate restriction in the Go 1.18 design.
Trying to put the brackets in the wrong place is a compile error. For example, this does not work:
Type parameters always come immediately after the name. For functions, that's after the function name and before (. For types, that's after the type name and before the underlying type. For receivers, the [T] part is the only place the parameter shows up on a method.
The single-letter convention isn't a language rule, but it's strong enough in the Go ecosystem that breaking it makes code look unusual. The standard names are short and reflect what the parameter represents:
| Name | Use | Example |
|---|---|---|
T | A generic "type" with no further meaning | func Identity[T any](v T) T |
K, V | Map key and value | func Keys[K comparable, V any](m map[K]V) []K |
E | Element of a collection | func First[E any](s []E) E |
R | Result type when different from input | func Map[T, R any](s []T, fn func(T) R) []R |
S | A slice type as a whole (when needed) | func Sort[S ~[]E, E any](s S) |
You can also spell parameters out when a single letter would be confusing. If a function is generic over an order type and you keep talking about orders in the body, Order is clearer than O:
The rule of thumb most Go codebases follow: use single letters when the role is obvious (a one-letter map key, a slice element), and use words when the role carries domain meaning. Item reads better than T in a cart that holds many kinds of items; K is fine in a generic map helper because everyone reading it knows "K means key."
There's one anti-pattern worth flagging. Don't pick a single letter just because it's shorter when the meaning isn't obvious. A function with [T, U, V, W any] becomes a puzzle. A function with [Customer, Order, Address any] reads itself.
The standard library leans heavily on single letters in the slices, maps, and cmp packages because those packages are intentionally domain-free. When you write your own generics with a clear domain ("a function that pairs a Customer with their Orders"), don't be shy about using words.
Cost: Naming has zero runtime cost; the compiler erases the names entirely after monomorphization. So the only consideration is readability. Pick the name a reader would write themselves.
A function can declare more than one type parameter. Inside the brackets, you list them separated by commas, optionally grouping consecutive parameters that share a constraint:
Both K and V use the same constraint (any), so they're grouped: [K, V any]. If they used different constraints, you'd write them separately: [K comparable, V any].
Here's a realistic two-parameter function. Given a slice of orders and a function that extracts a price from each, return the total. The order type is T, the price type is R, and the extractor function maps one to the other:
The constraint on R (int | float64) is a sneak peek at the union-type syntax. For now, read it as "R must be one of these two types so the += operation works." T has no such restriction because we never operate on T directly; we just hand it to fn.
A two-parameter signature is also where pair-like helpers come up. A common one is Zip, which takes two slices and pairs them position by position:
The shape of a two-parameter function is worth visualizing. Each call site supplies two concrete types, one for each parameter, in the order they appear in the brackets:
The first concrete type in the call site goes to the first parameter in the declaration, the second to the second, and so on. There's no by-name binding; it's strictly positional, like regular function arguments. If you swap them at the call site, you swap the meanings of A and B for that call.
That positional rule is also why order matters. Pair[Key, Value] and Pair[Value, Key] are different generic signatures from the compiler's point of view. A function that takes a Pair[string, int] won't accept a Pair[int, string]. Pick an order that matches the domain and stick to it.
A small but useful pattern when you have many parameters with the same constraint: group them. These two are equivalent:
When constraints differ, separate them:
The structure: groups separated by commas, parameters sharing a constraint can sit together.
Functions are one place type parameters appear; types are the other. The placement is the same idea, just applied to a type declaration instead of a function.
Box is now a generic type. To use it, you supply a concrete type for T. Every reference to Box outside the declaration needs the brackets filled in:
Box[int] and Box[string] are two distinct types. You can't assign one to the other. They share a source-level definition but compile down to separate, fully-typed structs.
Generic types come up in places where you used to write interface{} and lose type information. A typed queue, a typed cache, a typed result wrapper. Here's a quick taste using a Result type that's generic over both success and error payloads:
The function's return type spells out the concrete T: Result[string]. You can't write Result on its own outside the declaration. The literal Result[string]{Value: name} also spells out the type parameter at construction. Type inference helps in some cases for functions, but for type construction in Go 1.22 you still write the parameter.
The type-parameter syntax for functions is the same syntax used for types, just applied to a different kind of declaration.
When you call a generic function, you can either spell out the type arguments explicitly or let the compiler figure them out from the regular arguments. Explicit comes first because it's the form that always works:
Min[int](3, 7) is unambiguous: T = int, and the regular arguments are checked against that. The square brackets at the call site mirror the square brackets at the declaration. Inside them go concrete types, in the same order they appeared in the declaration.
Most of the time you don't need to write them. The compiler can infer the type arguments from the regular arguments. Calling Min(3, 7) works exactly the same: the compiler sees two int arguments, looks at the declaration, deduces T = int, and proceeds.
Inference is the rule rather than the exception in real code. The few times you have to be explicit:
| Situation | Why |
|---|---|
| No regular argument has the type parameter | The compiler can't infer from nothing |
| You want a specific type that doesn't match any argument | Forcing the choice |
| The inferred type is ambiguous between two valid choices | Tell the compiler which one |
A common case for the first row is a function whose type parameter only appears in the return type. Without arguments to drive inference, the call site must say what it wants:
Zero has no parameters whose types could hint at T. The only place T shows up is the return type, and Go doesn't infer from return-context (the type you'd assign the result to). So every call must spell T out.
The opposite case, where the compiler can fill in a type that's "more general" than what you wanted, is also common. Calling Min(3, 7.5) won't compile because the compiler can't find a single T that's both int and float64. Explicit instantiation, Min[float64](3, 7.5), would force T = float64 and let 3 convert. For this chapter, the only rule to remember is: explicit always works, inference is the default, and explicit instantiation is needed when the compiler tells you it can't decide.
A type parameter is a name. Like any name, it has a scope. The rule for type parameters is straightforward: a parameter declared in [T any] is visible inside the declaration it belongs to, and nowhere else.
For a function, that means T is in scope from the opening [ through the closing } of the function body. Outside the function, T is not a thing:
Trying to declare var lastValue T at package scope would give undefined: T. The name lives inside Last only.
For a generic type, the scope of the parameter extends through the type declaration and through methods declared on the type. A method on Box[T] can mention T freely because the receiver (b Box[T]) re-introduces it:
Inside Get, T is whatever Box's T was at construction (here, string). The body can declare locals of type T, accept it as a parameter (if the method had more), or return it.
Type parameters can also shadow names. If you have a package-level type T, declaring a function with [T any] shadows it inside that function:
Inside describe, T refers to the type parameter, not the package-level struct. This works, but it's a smell. Pick parameter names that don't collide with types you might want to reference. The Go standard library avoids the issue by sticking to single letters and short conventions: there's almost never a package-level T to shadow.
One more subtle case. The parameter is local to the declaration, which means two different functions can each declare their own T and they refer to different things:
The T in First and the T in Last are separate parameters that happen to share a name. The compiler treats them independently. You can call First with T = string and Last with T = float64 in the same program without any conflict.
Cost: Scope rules are a compile-time concept. The compiler resolves every reference to T to a specific concrete type at each call site (a process called monomorphization, or in Go's implementation a hybrid called the GC shape stenciling). There's no runtime scope to maintain.
any vs interface{}The constraint any is the name to learn. Before Go 1.18, the only way to say "any type" was to write interface{} (an empty interface, which every type satisfies). When generics arrived, the language added any as an alias for interface{} to make generic code less noisy.
Echo1 and Echo2 compile to the same code. any and interface{} are interchangeable at every spot in the language: as a type, as a constraint, as a field, as a parameter. You can declare var v any or var v interface{} and get the same variable.
The language reference puts it precisely: any is a predeclared identifier defined as type any = interface{}. The gofmt tool prefers any and rewrites interface{} to any in most positions. The official Go style guide recommends any for new code. There is no behavioral difference; pick any because it's shorter and matches the standard library.
A few places interface{} still shows up: very old codebases that haven't been updated, code that needs to compile against Go versions before 1.18, and a couple of generated-code paths. If you're starting from a recent Go version, write any everywhere.
The constraint any here means "the cart can hold elements of any type." Each instantiation (Cart[string], Cart[int], Cart[Order]) creates a separate concrete type. You couldn't put a Mug and a 42 in the same Cart[string] because the second value isn't a string. Compare that to the pre-generics world, where you'd write []interface{} and lose all type checking on the contents.
Cost: A value held in a non-generic interface{} field or slice carries a runtime type tag and may be boxed. A value held in a generic T field, even when T = interface{}, follows the same rules because any and interface{} are literally the same type. The win from generics isn't a smaller memory footprint, it's compile-time type checking.
comparableThe other predeclared constraint is comparable. It matches any type whose values can be compared with == and !=. That includes the obvious ones (numbers, strings, booleans, pointers, channels) and excludes the not-comparable ones (slices, maps, functions, and structs that contain any of those).
The single most useful place for comparable is map keys. A map key must be comparable, so any generic function that constructs a map needs its key parameter constrained that way:
K is comparable because we use it as a map key. V is any because we never compare values, we just store them. The same function works whether you key by int, string, or any other comparable type.
Try replacing comparable with any and the code stops compiling. The compiler points right at the line that uses K as a map key:
The error message is the giveaway: a map key needs comparable. The same rule applies to any operation that uses == or !=. If you want to write a generic Contains function that checks whether a slice contains a value, you need comparable because the body will do if items[i] == target:
If T were any, the line item == target would fail to compile because there's no guarantee that T supports ==. comparable is the constraint that promises it does.
A small but important detail. The constraint comparable does not allow type-asserting through interfaces. Two interface{} values can be compared with == at runtime, but doing so panics if either holds an incomparable concrete type. Go's comparable constraint, before Go 1.20, did not accept interface types at all to prevent that class of bug. Since Go 1.20, it has been relaxed so that interface types satisfy comparable, but the runtime panic remains a hazard. If you constrain K comparable and someone calls your function with K = any, comparing two values that contain incomparable concrete types will panic at runtime. Sticking to plain comparable types (numbers, strings, IDs) avoids that corner.
Cost: comparable is a constraint, not a runtime check. The compiler decides whether T satisfies it at compile time. There's no overhead at the call site beyond what regular == would cost on a concrete type.
10 quizzes