Menu

Golang Generics: Type Parameters, Constraints and Examples

Generics (Go 1.18+) let one function or type work with many types while staying type-safe. Learn type parameters, the any, comparable and cmp.Ordered constraints, custom constraints with ~, generic types, and when not to use them.

This page includes runnable editors - edit, run, and see output instantly.

A generic function

Type parameters go in square brackets before the regular parameters. Each one has a constraint that says which types are allowed.

T and U are inferred from the arguments, so you rarely write Map[int, string](...). Inference works from function arguments; it cannot infer a type that appears only in the result. For func Zero[T any]() T, you must write Zero[int]().

Before Go 1.18 this function had to be written once per type, or take []interface{} and lose type safety.

Constraints

A constraint is an interface. It limits which types can be used and, in return, tells the compiler which operations are allowed on values of that type.

ConstraintAllowsLets you use
anyevery typeassignment, passing, storing
comparabletypes that support ====, !=, map keys
cmp.Ordered (Go 1.21)integers, floats, strings<, <=, >, >=, plus ==
an interface with methodstypes with those methodscalling the methods
a type union like ~int | ~float64exactly those typesoperators all of them support

With any instead of comparable, v == target fails to compile: invalid operation: v == target (incomparable types in type set). The constraint is what makes the operator legal.

These two helpers already exist: slices.Index and slices.Max. The standard slices, maps and cmp packages cover most everyday generic needs, so check them before writing your own.

Custom constraints with type unions and ~

A constraint can list types with |. The operators allowed are the ones every listed type supports.

The tilde matters. ~int64 means "any type whose underlying type is int64", so the named type Cents is accepted. Written as plain int64, Sum([]Cents{...}) fails with Cents does not satisfy Number (possibly missing ~ for int64 in Number).

Interfaces containing type unions can only be used as constraints, never as ordinary variable types. var n Number does not compile.

The golang.org/x/exp/constraints package has ready-made Integer, Float and Signed constraints. It is outside the standard library, so the runner here cannot import it; cmp.Ordered is the standard one.

Generic types

Structs, slices, maps and other types can have type parameters too. Methods on a generic type use the receiver's parameters.

Two details in this example come up constantly:

  • The zero value of T. var zero T is how you return "nothing" from generic code. There is no T{} or nil that works for every type.
  • Instantiation. A generic type must be instantiated before use: Stack[string], not Stack. Inside its own methods, the receiver is written Stack[T].

Go 1.24 added generic type aliases: type Set[T comparable] = map[T]struct{} now compiles.

Limits you will run into

  • No type parameters on methods. func (s *Stack[T]) Map[U any](...) is not allowed. Use a top-level function.
  • No specialization. You cannot write a separate implementation for T = string. A type switch on any(v) inside the function is the workaround, and usually a hint that generics are the wrong tool.
  • No operator constraints by name. You cannot say "any type with a + method"; you list the types in a union.
  • Field access through a constraint does not work. Even if every type in a union has a field ID, v.ID is not allowed. Use a method in the constraint instead.

When not to use generics

Generics fit container types (stacks, sets, caches, trees) and algorithms over slices and maps that do the same thing for every element type. They do not fit everywhere.

  • One concrete type. If the function is only ever called with []User, a generic version adds reading cost and nothing else.
  • Behavior that differs by type. That is what interfaces are for. A function taking an io.Writer is simpler than one taking [T io.Writer], and works the same.
  • Replacing interface parameters. func Print[T fmt.Stringer](v T) is no better than func Print(v fmt.Stringer).

The Go team's own guideline: write the code for a specific type first, and reach for type parameters when you notice yourself writing the same code a second time with only the types changed.

Common mistakes

  • Using any where comparable or cmp.Ordered is needed. The compiler rejects == or < on an any type parameter.
  • Forgetting ~. User-defined types like type ID int fail a plain int constraint.
  • Returning nil for a type parameter. Not allowed unless the constraint limits T to pointer-like types. Return a zero value.
  • Writing a generic helper that already exists. Check slices, maps and cmp first.

Frequently Asked Questions

Does Go have generics?

Yes, since Go 1.18 (March 2022). Functions and types can declare type parameters in square brackets: func Map[T, U any](s []T, f func(T) U) []U. Each type parameter has a constraint, which is an interface describing what the type must support.

What is the difference between any and comparable in Go generics?

any allows every type but lets you do almost nothing with values except assign, pass and store them. comparable allows only types that support == and !=, which is what you need for map keys or equality checks. For < and >, use cmp.Ordered.

What does the tilde (~) mean in a Go constraint?

~T means "any type whose underlying type is T". ~int matches int and also type Celsius int. Without the tilde, int in a constraint matches only int itself, so user-defined types would be rejected.

Can Go methods have type parameters?

No. A method can use the type parameters of its receiver type (func (s *Stack[T]) Push(v T)), but it cannot declare new ones. Write a top-level generic function instead: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED