Skip to content

Generics: type parameters and constraints

Generics let you write a single function or type that works across many types, while keeping full compile-time type safety. Where an interface abstracts over behaviour, a generic abstracts over the type itself — no any, no runtime type assertions, no boxing.

Type parameters on functions

A function gains type parameters in square brackets before the ordinary parameter list. Each type parameter has a constraint that limits which types may be substituted.

func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

fmt.Println(Max(3, 7))         // output: 7
fmt.Println(Max("go", "py"))   // output: py

T is the type parameter; cmp.Ordered is its constraint — the set of types that support <, >, and so on. The same Max now works for ints, floats, and strings, each checked at compile time.

Type inference

You usually don't write the type argument — the compiler infers T from the call's arguments. You can spell it out when inference can't (or for clarity):

fmt.Println(Max(3, 7))         // inferred: T = int
fmt.Println(Max[float64](3, 7)) // explicit: T = float64 → prints 7

Constraints are interfaces

A constraint is just an interface used in a type-parameter position. The two built-in ones you'll meet first:

  • any — no restriction (every type qualifies; it's literally interface{})
  • comparable — types that support == and !=
func Index[T comparable](s []T, target T) int {
    for i, v := range s {
        if v == target {     // == is allowed because T is comparable
            return i
        }
    }
    return -1
}

fmt.Println(Index([]string{"a", "b", "c"}, "b"))   // output: 1

Custom constraints: type sets and ~

A constraint interface can list a set of types with |. That lets the body use operators those types share. The ~ prefix means "any type whose underlying type is this," so your own defined types qualify too.

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

func Sum[T Number](nums []T) T {
    var total T          // zero value of T
    for _, n := range nums {
        total += n       // + is allowed: every type in the set supports it
    }
    return total
}

type Celsius float64     // underlying type is float64
fmt.Println(Sum([]int{1, 2, 3}))            // output: 6
fmt.Println(Sum([]Celsius{1.5, 2.5}))       // output: 4

Without the ~, Sum[Celsius] would be rejected — Celsius is not literally float64, only based on it:

type StrictFloat interface{ float64 }   // no ~

func StrictSum[T StrictFloat](xs []T) T { /* ... */ }

StrictSum([]Celsius{1, 2})
// compile error: Celsius does not satisfy StrictFloat
//   (possibly missing ~ for float64 in StrictFloat)

The compiler even suggests the fix. Add the ~ and Celsius qualifies.

Generic types

Types take type parameters too. The classic example is a container that holds any element type:

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

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

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

var s Stack[int]
s.Push(1)
s.Push(2)
v, ok := s.Pop()
fmt.Println(v, ok)   // output: 2 true

Note var zero T — since you don't know T, that's how you produce its zero value. Methods on a generic type repeat the type parameter in the receiver: (s *Stack[T]).

A generic set

Combining a generic type with comparable gives a reusable set — better than re-coding map[T]struct{} for each element type:

type Set[T comparable] map[T]struct{}

func (s Set[T]) Add(v T)      { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }

s := Set[string]{}
s.Add("go")
fmt.Println(s.Has("go"), s.Has("py"))   // output: true false

When not to reach for generics

Generics shine for containers and algorithms that are identical across element types (collections, Map/Filter/Reduce, min/max). They are not a replacement for interfaces: when you want different types to supply different behaviour behind one abstraction, that's an interface's job. Rule of thumb — if the only thing varying is the type, use a generic; if the behaviour varies, use an interface.

From Python: this is typing.TypeVar / Generic[T] territory, but enforced by the compiler rather than by an optional checker — and with zero runtime cost, since the types are resolved at build time.

Quick reference

Form Meaning
func F[T any](x T) function with a type parameter
[T cmp.Ordered] constraint allowing <, >
[T comparable] constraint allowing ==, !=
interface{ ~int \| ~float64 } type-set constraint; ~ = underlying type
type Box[T any] struct{ v T } generic type
func (b Box[T]) Get() T method on a generic type
var zero T the zero value of a type parameter

Sources