Generics arrived in Go 1.18 (March 2022). They let you write functions and types that work on a parametric set of types, with compile-time type checking.

Type parameters

A type parameter is declared in square brackets after the function or type name:

func Max[T int | float64 | string](a, b T) T {
    if a > b {
        return a
    }
    return b
}

fmt.Println(Max(1, 2))           // T inferred as int
fmt.Println(Max(1.5, 0.5))       // T inferred as float64
fmt.Println(Max("a", "b"))       // T inferred as string

The thing inside the brackets after the T is called a constraint.

Constraints

A constraint is just an interface. It can list methods, like a regular interface, and type sets:

// matches any integer type
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

// the leading ~ means "or any type whose underlying type is X",
// so `type Celsius float64` also satisfies a ~float64 constraint.

The standard library ships golang.org/x/exp/constraints with Ordered, Integer, Float, Signed, Unsigned. You can use it directly:

import "golang.org/x/exp/constraints"

func Min[T constraints.Ordered](a, b T) T {
    if a < b { return a }
    return b
}

Built-in constraints you'll see often:

  • any (alias for interface{}).
  • comparable — types you can use with == and !=. Required for map keys and for any generic that compares values.
func Index[T comparable](s []T, x T) int {
    for i, v := range s {
        if v == x { return i }
    }
    return -1
}

Generic types

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

func (s *Stack[T]) Push(v T)   { s.data = append(s.data, v) }
func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.data) == 0 { return zero, false }
    v := s.data[len(s.data)-1]
    s.data = s.data[:len(s.data)-1]
    return v, true
}

Used as:

s := Stack[int]{}
s.Push(1); s.Push(2)
v, ok := s.Pop()

You'll see this pattern again in Part 2.

When to use generics

  • Containers (Stack[T], Set[T], Cache[K,V]).
  • Algorithms over types that share an operator (Min, Max, Sum).
  • Function utilities (Map, Filter, Reduce).

When not to use them:

  • If a single concrete type is enough, don't reach for generics.
  • If you only need behavior, an interface is simpler and clearer.
  • Generic code is harder to read; reserve it for genuine reuse.

Type inference and explicit instantiation

The compiler infers T from the arguments when possible:

Max(1, 2)              // inferred
Max[float64](1, 2)     // explicit, here it forces conversion

For generic types you must instantiate:

s := Stack[int]{}

Method sets and generics

You cannot add type parameters to methods of an existing type. Only the type itself can have them. This means common patterns like generic visitor methods don't work directly — you usually convert a method to a function.

Run the example

go run ./part1-language/examples/09_generics