Generics | Go - Wyatt's Notes
Introduction
Section titled “Introduction”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.
Type Parameters
Section titled “Type Parameters”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)})Constraints
Section titled “Constraints”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)}Built-in Constraints
Section titled “Built-in Constraints”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)) // 7fmt.Println(Max(3.14, 2.71)) // 3.14fmt.Println(Max("a", "z")) // zcmp.Ordered is defined as:
type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string}Custom Constraints
Section titled “Custom Constraints”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.
Methods in Constraints
Section titled “Methods in Constraints”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 constraints Package
Section titled “The constraints Package”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:
| Constraint | Types Allowed |
|---|---|
constraints.Signed | ~int``~int8``~int16``~int32``~int64 |
constraints.Unsigned | ~uint``~uint8``~uint16``~uint32``~uint64``~uintptr |
constraints.Integer | All signed and unsigned integers |
constraints.Float | ~float32``~float64 |
constraints.Ordered | All integers, floats, and ~string |
constraints.Complex | ~complex64``~complex128 |
Generic Types
Section titled “Generic Types”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 trueGeneric Slices
Section titled “Generic Slices”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}Generic Maps
Section titled “Generic Maps”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}Type Inference
Section titled “Type Inference”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 fnGeneric Methods
Section titled “Generic Methods”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.
Instantiation
Section titled “Instantiation”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.
Limitations
Section titled “Limitations”No specialization. You cannot provide different implementations for different types. Generic code is the same for all type arguments.
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.”No variadic type parameters. Type parameter lists must be fixed-length.
No type parameter defaults. Each type parameter must be specified or inferred.
Methods cannot add type parameters. Only the type’s own type parameters are available in methods.
Intuition
Section titled “Intuition”Generics are like templates with type safety: Imagine writing a recipe that works for any ingredient — flour, sugar, or chocolate. Instead of writing three separate recipes, you write one generic recipe that works for all. Generics do the same for code: write a function or data structure once, use it with any type, and the compiler ensures type safety at compile time.
Why it matters: Before Go 1.18, you either wrote duplicate code for each type or used interface{} and lost type safety. Generics give you the best of both worlds: code reuse without sacrificing compile-time guarantees. They’re essential for writing generic data structures (trees, graphs, queues) and utility functions (Map, Filter, Reduce).
The key insight: Constraints are the key to generics — they define what operations a type must support. A Max[T cmp.Ordered] function works because cmp.Ordered guarantees the type supports comparison operators.
Common Pitfalls
Section titled “Common Pitfalls”Using
anyas a constraint when a narrower constraint exists.anyallows any type, including types that do not support the operations your function performs. Usecomparablecmp.OrderedOr a custom constraint to enforce requirements at compile time.Forgetting
comparablefor map keys and set elements. If a generic type uses a value as a map key, the constraint must includecomparable.Confusing
~TwithT.~intmatchesintand any type with underlying typeint(e.g.,type MyInt int).intmatches onlyintexactly.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.
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.
flowchart TD
A[Generics] --> B[Key Concepts]
A --> C[Core Principles]
A --> D[Practical Applications]
B --> E[Fundamental definitions]
C --> F[Design patterns]
D --> G[Real-world usage]Summary
Section titled “Summary”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
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Interfaces: Interface patterns that generics can complement or replace.
- Error Handling: Error handling patterns that work with generic functions.
- Practice Generics: Practice problems covering generic programming.