Generic Types and Constraints

Types can have type parameters too. A Stack[T], Set[T] or Cache[K, V] is written once and used with any element type, and the compiler keeps each use type-safe. This lesson covers declaring generic types, their methods, constraints on type parameters, constraints that refer to themselves, and generic type aliases.

A generic stack

GO
package main

import "fmt"

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
	}
	v := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return v, true
}

func (s *Stack[T]) Len() int { return len(s.items) }

func main() {
	var words Stack[string]
	words.Push("first")
	words.Push("second")
	w, _ := words.Pop()
	fmt.Println(w, words.Len())

	nums := &Stack[int]{}
	nums.Push(42)
	n, ok := nums.Pop()
	_, ok2 := nums.Pop()
	fmt.Println(n, ok, ok2)
}
Output
second 1
42 true false

The type's parameters are declared once, after its name. Methods repeat the parameter names in the receiver — (s *Stack[T]) — and can use T throughout. A Stack[string] and a Stack[int] are different types; pushing an int onto a Stack[string] is a compile error. var zero T is the idiomatic way to get the zero value of a type parameter.

A generic set

Go has no built-in set, but a generic one is a few lines on top of a map. The element type must be comparable because it's used as a map key:

GO
package main

import (
	"cmp"
	"fmt"
	"maps"
	"slices"
)

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

func NewSet[T comparable](items ...T) *Set[T] {
	s := &Set[T]{m: make(map[T]struct{})}
	for _, it := range items {
		s.Add(it)
	}
	return s
}

func (s *Set[T]) Add(v T)      { s.m[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok }
func (s *Set[T]) Len() int     { return len(s.m) }

func (s *Set[T]) Intersect(other *Set[T]) *Set[T] {
	out := NewSet[T]()
	for v := range s.m {
		if other.Has(v) {
			out.Add(v)
		}
	}
	return out
}

// Sorted is a function, not a method, because it needs a stricter
// constraint (cmp.Ordered) than the Set type itself (comparable).
func Sorted[T cmp.Ordered](s *Set[T]) []T {
	return slices.Sorted(maps.Keys(s.m))
}

func main() {
	a := NewSet("go", "rust", "zig")
	b := NewSet("python", "go", "zig")
	fmt.Println(a.Has("go"), a.Has("java"), a.Len())
	fmt.Println(Sorted(a.Intersect(b)))
}
Output
true false 3
[go zig]

Methods can't add extra constraints to the type's own parameters, which is why Sorted is a plain function here.

Several type parameters

GO
package main

import "fmt"

type Pair[K comparable, V any] struct {
	Key   K
	Value V
}

func (p Pair[K, V]) String() string { return fmt.Sprintf("%v=%v", p.Key, p.Value) }

type Cache[K comparable, V any] struct {
	data  map[K]V
	limit int
	order []K // keys in insertion order, oldest first
}

func NewCache[K comparable, V any](limit int) *Cache[K, V] {
	return &Cache[K, V]{data: map[K]V{}, limit: limit}
}

func (c *Cache[K, V]) Put(k K, v V) {
	if _, exists := c.data[k]; !exists {
		if len(c.order) == c.limit { // full: evict the oldest
			delete(c.data, c.order[0])
			c.order = c.order[1:]
		}
		c.order = append(c.order, k)
	}
	c.data[k] = v
}

func (c *Cache[K, V]) Get(k K) (V, bool) {
	v, ok := c.data[k]
	return v, ok
}

func main() {
	fmt.Println(Pair[string, int]{"port", 8080})

	c := NewCache[string, float64](2)
	c.Put("EUR", 1.0)
	c.Put("USD", 1.17)
	c.Put("GBP", 0.87) // evicts EUR
	_, hasEUR := c.Get("EUR")
	gbp, _ := c.Get("GBP")
	fmt.Println(hasEUR, gbp)
}
Output
port=8080
false 0.87

Constraints that mention the type itself

Sometimes a constraint needs to say "a type with a method that takes and returns that same type". Since Go 1.26, a generic type's own name may appear in its type parameter list, which makes this direct:

GO
package main

import "fmt"

type Adder[A Adder[A]] interface {
	Add(A) A
}

func SumAll[A Adder[A]](items []A) A {
	var total A
	for _, it := range items {
		total = total.Add(it)
	}
	return total
}

type Vec struct{ X, Y float64 }

func (v Vec) Add(o Vec) Vec { return Vec{v.X + o.X, v.Y + o.Y} }

type Money struct{ Cents int }

func (m Money) Add(o Money) Money { return Money{m.Cents + o.Cents} }

func main() {
	fmt.Println(SumAll([]Vec{{1, 2}, {3, 4}, {0.5, 0}}))
	fmt.Println(SumAll([]Money{{250}, {199}}))
}
Output
{4.5 6}
{449}

Generic type aliases

A type alias can have type parameters too. That's mainly useful when moving types between packages or giving a long instantiation a shorter name:

GO
package main

import "fmt"

type Pair[K comparable, V any] struct {
	Key   K
	Value V
}

type Entry[V any] = Pair[string, V] // an alias with its own parameter

func main() {
	e := Entry[int]{"retries", 3}
	var p Pair[string, int] = e // same type, no conversion needed
	fmt.Printf("%+v %T\n", p, e)
}
Output
{Key:retries Value:3} main.Pair[string,int]

Limits to know

  • You can't use a type switch or type assertion on a value of a type parameter's type directly; convert it to any first.
  • Interface types can't have generic methods (but since Go 1.27, other types' methods can — next lesson).
  • A generic type must be instantiated before use: Stack[int], not Stack. When calling a generic function, the type arguments can usually be inferred.

Try it yourself

Write a generic Queue[T] with Enqueue, Dequeue and Len, and use it with two different element types.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
serving 101 - waiting: 1
anyone waiting? false
Next: generic methods, new in Go 1.27.