Generics
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
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
[T int | float64] declares T as a type parameter, constrained to either int or float64 — T 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
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, 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
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.
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.
| Approach | Type safety | Code reuse |
|---|---|---|
| Separate function per type | Full | None — duplicated logic |
| any / interface{} | None at compile time | Full, but unsafe |
| Generics — func F[T Constraint](...) | Full | Full |
Coding Challenges
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 solutionWrite 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 solutionUsing 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 solutionChapter 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)