Generics

Course 3 · Ch 1
Generics: Type Parameters and Constraints
Writing one function or type that works correctly across many concrete types, without giving up Go's compile-time type safety

Chapter 5 (Fundamentals) showed that sum(numbers ...int) only works for int — a separate function would be needed for float64, despite the logic being identical. Generics, added to Go relatively recently, solve exactly this: one function definition, usable with multiple types, fully checked at compile time. Unlike Intermediate Chapter 2's interfaces (which work via shared methods), generics work via shared capability — "any type that supports +", for example.

The Problem Without Generics

func sumInts(numbers []int) int { total := 0 for _, n := range numbers { total += n } return total } func sumFloats(numbers []float64) float64 { total := 0.0 for _, n := range numbers { total += n } return total }

These two functions are identical except for one type. Before generics, this duplication was simply accepted, or worked around with any (Intermediate Chapter 2) at the cost of losing type safety and needing manual type assertions.

A Generic Function with a Type Parameter

func Sum[T int | float64](numbers []T) T { var total T for _, n := range numbers { total += n } return total } fmt.Println(Sum([]int{1, 2, 3})) // 6 fmt.Println(Sum([]float64{1.5, 2.5})) // 4

[T int | float64] declares T as a type parameter, constrained to either int or float64T stands in for whichever concrete type is actually used at the call site. var total T uses Chapter 2's zero-value behaviour generically: it's 0 when T is int, 0.0 when T is float64, decided automatically. Go infers T from the argument — there's no need to write Sum[int](...) explicitly in normal use.

Named Constraints

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

Inline constraints like int | float64 get repetitive across several generic functions, so they're usually pulled out into a named constraint interface instead — an interface listing allowed underlying types rather than methods, reusable by name across the whole package.

comparable — a built-in constraint
Go provides a built-in constraint called comparable, satisfied by any type that supports == and != — commonly used for generic functions that need to check for a specific value inside a collection, covered in this chapter's challenges.

A Generic Type — Not Just a Generic Function

type Stack[T any] struct { items []T } func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) } func (s *Stack[T]) Pop() T { last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last } intStack := &Stack[int]{} intStack.Push(10) intStack.Push(20) fmt.Println(intStack.Pop()) // 20

Structs (Intermediate Chapter 1) can be generic too: Stack[T any] declares a stack that works with whatever single type T is chosen at creation time — Stack[int]{} here. Every method automatically carries the same T, so Push and Pop stay perfectly type-safe for whichever type was chosen, with no any-style runtime checking required anywhere inside.

Generics are not the same as any
any (Intermediate Chapter 2) gives up type information entirely — the compiler can't help at all. Generics keep full compile-time type checking; Stack[int] and Stack[string] are still fully separate, type-safe usages, just generated from one shared definition.
ApproachType safetyCode reuse
Separate function per typeFullNone — duplicated logic
any / interface{}None at compile timeFull, but unsafe
Generics — func F[T Constraint](...)FullFull

Coding Challenges

Challenge 1

Write a generic function Max[T int | float64](a, b T) T that returns the larger of two values. Call it once with two ints and once with two float64s, printing both results.

📄 View solution
Challenge 2

Write a generic function Contains[T comparable](items []T, target T) bool that returns true if target appears anywhere in items. Test it once with a []string and once with a []int.

📄 View solution
Challenge 3

Using the Stack[T any] type from this chapter, create a Stack[string], push 3 names onto it, then pop and print all 3 (which should come back out in reverse order).

📄 View solution

Chapter 1 Quick Reference (Advanced)

  • func F[T Constraint](x T) T { } — a generic function with type parameter T
  • [T int | float64] — inline constraint: T must be one of these listed types
  • type Name interface { typeA | typeB } — a reusable, named constraint
  • comparable — built-in constraint for types supporting == and !=
  • any — the loosest constraint; equivalent to no constraint at all
  • type Name[T any] struct { } — a generic type; methods carry the same T automatically
  • Generics ≠ any — full compile-time type checking is preserved throughout
  • Next chapter: error wrapping — errors.Is, errors.As, and fmt.Errorf("%w", err)