Learn Go generics with compiling examples covering constraints, type inference, comparable values, generic data structures, and common mistakes.
Last updated on

Generics in Go: Type Parameters, Constraints, and Practical Patterns


Generics let Go functions and types work with a set of types while keeping compile-time type safety. The important part is the constraint: it tells the compiler which operations are valid for a type parameter.

That is why this does not compile:

func Add[T any](a, b T) T {
    return a + b
}

any permits every type, including types that do not support +. A correct numeric version needs a constraint that permits addition.

Define a useful type constraint

type Number interface {
    ~int | ~int64 | ~float64
}

func Add[T Number](a, b T) T {
    return a + b
}

The ~ means “this type or any defined type with this underlying type.” Without it, a custom type such as type Duration int64 would not satisfy the constraint.

type Duration int64

fmt.Println(Add(2, 3))
fmt.Println(Add(Duration(10), Duration(5)))

Go normally infers T from the arguments, so callers rarely need to write Add[int](2, 3) explicitly.

Use comparable for map keys and equality

The predeclared comparable constraint accepts types that support == and !=. It is useful for sets and lookup helpers.

func Contains[T comparable](values []T, target T) bool {
    for _, value := range values {
        if value == target {
            return true
        }
    }
    return false
}

Slices, maps, and functions are not comparable, so Contains rejects them at compile time rather than failing later.

Build a generic stack without hiding errors

A generic type can store its type parameter in fields and use it across methods.

type Stack[T any] struct {
    values []T
}

func (s *Stack[T]) Push(value T) {
    s.values = append(s.values, value)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.values) == 0 {
        var zero T
        return zero, false
    }

    last := len(s.values) - 1
    value := s.values[last]
    s.values = s.values[:last]
    return value, true
}

Returning (T, bool) matters. Returning only the zero value cannot distinguish an empty Stack[int] from a stack whose top value is legitimately 0.

var stack Stack[string]
stack.Push("first")
stack.Push("second")

value, ok := stack.Pop()
fmt.Println(value, ok) // second true

Constraints can require methods

Constraints are interfaces, so they can require behavior as well as a type set.

type Stringer interface {
    String() string
}

func JoinStrings[T Stringer](values []T) string {
    parts := make([]string, 0, len(values))
    for _, value := range values {
        parts = append(parts, value.String())
    }
    return strings.Join(parts, ", ")
}

This looks similar to accepting []fmt.Stringer, but it preserves the concrete slice element type. Read interfaces in Go before reaching for a type parameter: ordinary interfaces are often the simpler tool when you only need shared behavior.

When generics are the right abstraction

Generics work well when the algorithm is identical across several types:

  • containers such as sets, stacks, and caches
  • slice and map transformations
  • numeric algorithms
  • reusable helpers where returning the original concrete type matters

They are less useful when each type needs different behavior. A type switch inside a generic function is often a sign that an interface or separate functions would be clearer.

Do not introduce a type parameter merely to avoid writing two short functions. The abstraction should remove meaningful duplication without making callers decode a complicated constraint.

Common generics mistakes

The first mistake is using any and then trying to perform operations that any does not promise. The second is writing an overly broad constraint when only one or two concrete types are required.

Also remember that a constraint’s type set and method set determine what the function body can do. Methods belonging to one possible concrete type are not automatically available on the type parameter.

Generic code still needs ordinary API design. Return useful error information, preserve zero-value behavior where practical, and keep constraints close to the operation they enable. For newer language features built on generics, see iterators in Go and generic type aliases. The official Go generics tutorial is a useful next reference.