Skip to main content
Generics

Generics

10 minutes read

Filed underGo Programming Languageon

Learn how Go generics enable type-safe code reuse through type parameters and constraints, eliminating duplicate functions and unsafe interface{} casts.

The problem before generics

Before Go 1.18, writing a function that worked across multiple types meant one of two bad choices. The first was duplication: write the same logic once for each type you needed:

The second was interface{} (now aliased as any) — which threw type safety out the window and forced callers into type assertions that could panic at runtime.

Generics solve this. They let you write one function that works for many types while the compiler verifies type correctness at every call site.

Type parameters

A generic function declares a type parameter list in square brackets before the regular parameter list:

T is the type parameter — a placeholder for a real type that gets filled in when the function is called. int | float64 | string is the constraint — it specifies which types are allowed to substitute for T. The function body uses T exactly as if it were a concrete type.

To call a generic function, the compiler needs to know what T is. You can supply it explicitly:

But in practice you almost never need to. Go infers the type argument from the call-site arguments.

Type inference

When Go can determine the type argument from the values you pass, you don't have to write it:

Type inference works in the vast majority of cases. Explicit type arguments are only needed when inference is ambiguous — which is rare in well-designed generic functions.

Constraints

A constraint limits which types can be substituted for a type parameter. Constraints are regular interfaces — every interface in Go is now a valid constraint, and interfaces gained new powers specifically to express type constraints. If you want a refresher on how interfaces work in Go before diving into constraint syntax, the interfaces article covers the fundamentals.

any

The any constraint (an alias for interface{}) accepts every type. Use it when your function only stores, passes, or returns the value — it doesn't need to perform any operations specific to the type:

First works for a slice of any element type. It doesn't compare or add elements — it only reads, so any is the right constraint.

comparable

The built-in comparable constraint accepts any type that supports == and !=. This includes all numeric types, strings, booleans, pointers, channels, arrays of comparable elements, and structs whose fields are all comparable.

The compiler enforces this: if you try to use == on a value whose type parameter has constraint any, it refuses to compile. comparable is the right constraint whenever your function compares values for equality.

Slices, maps, and functions are not comparable

Even though slices and maps are the most common composite types in Go, they cannot satisfy comparable — the language does not allow comparing them with ==. Passing []int or map[string]int as a type argument to a comparable-constrained function is a compile error. If you need to use a slice as a map key or deduplicate a slice of slices, you'll need to convert to a string key or use a different strategy.

Interface constraints

Any interface that defines methods can serve as a constraint. The function body can then call those methods on values of the type parameter:

The compiler knows that every type substituted for T implements Stringer, so calling item.String() is valid.

Union constraints

Interfaces can include a type set — concrete types separated by |. The type parameter can only be substituted with types listed in the set, and the function body may only use operations supported by all of them:

The += operator is valid here because every type in Number supports addition. If you tried to use an operation that only some of the types support, the compiler would reject it.

The ~ prefix

A constraint element prefixed with ~ matches any type whose underlying type matches:

Without ~, Celsius would not satisfy a constraint of float64 because it is a distinct named type. The ~ prefix extends the constraint to cover all named types built on top of that underlying type — a critical tool for writing constraints that work with user-defined types.

cmp.Ordered

The standard library's cmp package (added in Go 1.21) exports an Ordered constraint covering all types that support the ordering operators (<, <=, >, >=):

cmp.Ordered expands to all integer types, float types, and string — with ~ applied to each, so user-defined types work too. Using it saves you from writing that union by hand.

Before Go 1.21

For Go versions before 1.21, the golang.org/x/exp/constraints package provides constraints.Ordered, constraints.Integer, constraints.Float, and others. They are conceptually identical — same union types, same ~ prefix.

Multiple type parameters

A type parameter list can include more than one parameter, each with its own constraint:

K must be comparable because maps require comparable keys. V can be any because the function never inspects the values — it only collects the keys.

Generic types

Generics apply to type declarations, not just functions. This is how you build type-safe container data structures:

The type argument is specified when declaring a variable of the generic type:

Stack[int] and Stack[string] are distinct types — the compiler generates separate code for each and enforces their type rules independently.

Methods cannot add new type parameters

A method defined on a generic type can use the type parameters declared on the type. It cannot declare new ones of its own. If you need additional type flexibility in a method, write a package-level generic function instead and pass the receiver as an argument.

The zero value of a type parameter

When a function needs to return "nothing" for an unknown type — on error, or when a search fails — you need the zero value of T. The pattern is a simple variable declaration:

var zero T initializes zero to the zero value of whatever type T turns out to be: 0 for numeric types, "" for strings, false for booleans, nil for pointers, slices, maps, and channels. This is the canonical way to express "zero value of a type parameter."

Generics in the standard library

Go 1.21 adopted generics throughout the standard library. The slices and cmp packages were added to provide utilities that previously required awkward sort.Interface implementations or unsafe interface{} casts.

The slices package covers the most common slice operations:

Before slices.Sort, sorting required sort.Slice with an index-based closure. With generics, the compiler simply verifies that the element type supports ordering — no boilerplate needed.

The cmp package provides comparison utilities alongside the Ordered constraint:

Browsing these packages is one of the most effective ways to learn idiomatic generic code: they solve real, everyday problems with minimal ceremony.

When to use generics

Generics shine when the same algorithm works across multiple types and the type information must flow from input to output. The clearest indicators are:

  • A function signature of the form []T → T, T → T, or map[K]V → []K
  • A data structure that must work with user-chosen element types
  • The same logic duplicated across types in your codebase

They are not a replacement for interfaces. If your function only needs to call methods on a value, use a regular interface — that is what interfaces are for:

The rule of thumb: if the type parameter appears in both the input and the output in a way that preserves type identity, generics are appropriate. If the type parameter only flows in and disappears into an interface, a regular interface is cleaner.