Skip to content

Generics

Go 1.18 (released March 2022) added generics via type parameters. Generics allow you to write Functions and types that abstract over different concrete types while maintaining full type safety At compile time.

A generic function declares type parameters in square brackets after the function name:

func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}

Call with explicit type arguments:

nums := []int{1, 2, 3}
strs := Map[int, string](nums, func(n int) string {
return strconv.Itoa(n)
})
fmt.Println(strs) // [1 2 3]

In most cases, the compiler infers type arguments:

strs := Map(nums, func(n int) string {
return strconv.Itoa(n)
})

A constraint is an interface that restricts which types can be used as a type argument. The any Constraint (alias for interface{}) allows any type:

func Print[T any](v T) {
fmt.Printf("%v\n", v)
}

The cmp package provides ordered type constraints:

import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
fmt.Println(Max(3, 7)) // 7
fmt.Println(Max(3.14, 2.71)) // 3.14
fmt.Println(Max("a", "z")) // z

cmp.Ordered is defined as:

type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}

Define constraints as interfaces with type elements:

type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}

The ~ token means “the underlying type must be.” A type MyInt int has underlying type intSo It satisfies ~int.

Constraints can include methods alongside type elements:

type Stringer interface {
~string | ~[]byte
String() string
}
func Describe[T Stringer](v T) {
fmt.Println(v.String())
}

This constraint requires the type to have an underlying type of string or []byte AND to have a String() string method.

The golang.org/x/exp/constraints package provides additional useful constraints:

import "golang.org/x/exp/constraints"
func Clamp[T constraints.Ordered](v, lo, hi T) T {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}

Common constraints:

ConstraintTypes Allowed
constraints.Signed~int``~int8``~int16``~int32``~int64
constraints.Unsigned~uint``~uint8``~uint16``~uint32``~uint64``~uintptr
constraints.IntegerAll signed and unsigned integers
constraints.Float~float32``~float64
constraints.OrderedAll integers, floats, and ~string
constraints.Complex~complex64``~complex128

Types can also have type parameters:

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) {
if len(s.items) == 0 {
var zero T
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)
}

Usage:

s := Stack[int]{}
s.Push(1)
s.Push(2)
v, ok := s.Pop()
fmt.Println(v, ok) // 2 true
type Set[T comparable] struct {
items map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{items: make(map[T]struct{})}
}
func (s *Set[T]) Add(v T) {
s.items[v] = struct{}{}
}
func (s *Set[T]) Contains(v T) bool {
_, ok := s.items[v]
return ok
}
type Pair[K comparable, V any] struct {
Key K
Value V
}
func MapKeys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}

Go uses type inference for generic functions. The compiler infers type arguments from function Arguments. When inference is ambiguous, you must specify explicitly:

func New[T any]() *T {
return new(T)
}
// Ambiguous -- no arguments to infer from:
var p *int = New[int]() // must specify
// Unambiguous -- inferred from argument:
s := Map(nums, fn) // T and U inferred from nums and fn

Methods on generic types can have their own type parameters (separate from the receiver”s type Parameters):

func (s *Stack[T]) Filter(predicate func(T) bool) *Stack[T] {
result := &Stack[T]{}
for _, v := range s.items {
if predicate(v) {
result.Push(v)
}
}
return result
}

Note: methods cannot introduce new type parameters that are not on the receiver. All type parameters Must be declared on the type.

Go uses monomorphization at compile time. Each unique set of type arguments produces a separate Specialization of the generic function or type. There is no boxing or type erasure at runtime.

  1. No specialization. You cannot provide different implementations for different types. Generic code is the same for all type arguments.

  2. No operator methods in constraints. You cannot require that a type support + beyond the built-in types. You cannot write a constraint that says “any type with a + operator.”

  3. No variadic type parameters. Type parameter lists must be fixed-length.

  4. No type parameter defaults. Each type parameter must be specified or inferred.

  5. Methods cannot add type parameters. Only the type’s own type parameters are available in methods.

Generics are molds, not copies: Before generics, writing a Max function for int and another for float64 meant maintaining two near-identical copies. Generics let you write one mold — the compiler stamps out type-specific versions at compile time, like a factory using the same blueprint to produce widgets in steel or plastic. No runtime cost, no boxing, just a single source of truth.

Why it matters: Generics eliminate the tension between code reuse and type safety. You no longer need to choose between any (unsafe) or copy-pasting (fragile) when writing collection utilities, algorithms, or data structures.

The key insight: Constraints are the “material specification” for your mold — they tell the compiler which operations are safe to perform on the generic type.

  1. Using any as a constraint when a narrower constraint exists. any allows any type, including types that do not support the operations your function performs. Use comparable cmp.OrderedOr a custom constraint to enforce requirements at compile time.

  2. Forgetting comparable for map keys and set elements. If a generic type uses a value as a map key, the constraint must include comparable.

  3. Confusing ~T with T. ~int matches int and any type with underlying type int (e.g., type MyInt int). int matches only int exactly.

  4. Overusing generics. Not every function needs to be generic. If a function only works with one or two concrete types, concrete implementations are clearer and often more efficient.

  5. Generic type assertion ambiguity. When type parameters appear in the same signature position as concrete types, inference may fail. Specify type arguments explicitly in such cases.

This topic covers the core concepts of generics, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • core concepts and terminology
  • algorithms and computational thinking
  • practical implementation
  • security and ethical considerations
  • applications in the real world

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.