Chapter 6: Generics
Introduces Go generics as a type-safe layer of abstraction and previews the chapter’s progression from type parameters to reusable collections and standard-library utilities.
After this chapter you will be able to
- Type-safe reuse through generics. Explain that Go generics use type parameters so functions, types, and interfaces can work with different data types without duplicating code or giving up compile-time type safety.
- Reusable type-safe filtering. Explain how one generic Filter function replaces otherwise identical functions for recipes, steps, ingredients, and other types without giving up compile-time type safety.
- Type parameter syntax and inference. Describe the square-bracket type parameter syntax, such as node[T any], and explain that the compiler can infer T for a generic function from its arguments.
- Generic collection contracts and constraints. Explain how a generic Collection interface keeps operations consistent with its chosen element type, and distinguish the unrestricted any constraint from comparable, which permits equality operations.
- Stringer interface constraint. Explain that StringLinkedList accepts only element types that implement fmt.Stringer, so its elements can be used for string output and their String() method can be called confidently.
- Custom numeric constraints. Explain that a custom interface can constrain generic code to listed numeric types, such as int and float32, but a new type such as MyInt defined from int does not satisfy that constraint unless it is listed.
- Underlying type constraints. Explain how ~ changes a type-set constraint from matching an exact type to accepting any type with that underlying type. Contrast a constraint listing int and float32 with one listing ~int and ~float32, including whether MyInt, whose underlying type is int, qualifies.
- Choosing type constraints. Explain how built-in interfaces, custom type sets, and the tilde operator let generic collections and algorithms accept the intended types while preserving type safety.
- Filter constraint and type inference. Explain why Filter uses the any constraint and how a call with a slice of Recipe values obtains its type argument.
- Generic linked list construction. Explain how a generic constructor such as NewLinkedList uses a type parameter to return a pointer to a new LinkedList of that type, and why its comparable constraint limits which types can be used.
- Functions over generic collections. Describe how PrintCollection accepts a Collection[T] to work with collections of different element types, and how it checks whether a collection also implements List[T] before accessing elements by index.
- Two type parameter transformations. Explain how a transformation function uses separate type parameters for its input and output types, so one function can handle conversions such as extracting string titles from Recipe values; describe how the compiler infers those types from the arguments.
- Generic list interface contract. Explain how List[T] defines operations on elements of type T and how LinkedList[T] implements that contract with methods, so callers can use the same interface with different element types.
- Linked list constraints and positions. Distinguish the interface's any constraint from LinkedList[T comparable], and describe how the implementation handles indexed access and changes. Explain why Get declares a zero value of T before it can return a value.
- Interface based list reversal. Describe how ReverseList takes a generic List[T] and reverses it in place by swapping elements in the first half with corresponding elements at the other end. Explain why using the interface makes the function reusable across list implementations and element types while retaining compile-time type safety.
- Independent key and value parameters. Explain how [K comparable, V any] gives a generic map separate key and value types, with a constraint on each: keys must support equality comparison, while values may be any type.
- Generic map interface and implementation. Describe how HashMap[K, V] implements Map[K, V] by wrapping Go’s built-in map, and name the key operations and utilities the section implements.
- Reusable map merging. Explain how MergeMaps uses the shared Map[K, V] interface to combine maps across key and value types, and why its key parameter must be comparable. State that the compiler can infer matching type arguments from the supplied maps at a call.
- Generics adoption tradeoff. Explain that generics enable abstraction and code reuse, but adopting them requires weighing those benefits against the simplicity and clarity of concrete, type-specific Go code.
- GC shape stenciling. Explain that Go generates a version of a generic function for each distinct GC shape, with pointer types sharing a version that may use an internal dictionary for method calls.
- Allocation risks and measurement. Describe how interface storage in generic code can cause heap escapes and GC pressure, and how narrow constraints, avoiding unnecessary conversions in hot loops, and profiling or benchmarking guide optimization.
- Constant and field restrictions. Describe why a type parameter cannot be used to declare a constant of a generic type or dynamically name a struct field.
- Operator constraints and type safety. Explain that arithmetic or comparison operators can be used on a type parameter only when its constraint permits those operations; for example, using + requires a constraint covering suitable numeric types. State that using + without such a constraint produces a compile-time error.
- Constraint method sets. Explain that a type parameter constrained by an interface exposes only the methods in that interface’s method set, even when a concrete type has additional methods.
- Generic interface assertions. Distinguish a runtime type assertion to a matching generic instantiation from an assertion to an instantiation that cannot implement the interface, which the compiler rejects.
- Type inference surprises. Explain that Go may infer an unexpected type or fail to infer one for a generic use, and that explicit type parameters can clarify the intended type.
- Reflection and runtime types. Explain why reflection cannot always recover a generic value’s original type arguments, and distinguish the dynamic type printed by %T for an interface value from the static type parameter T.
- Good uses for generics. Explain when generics earn their complexity: identical logic across multiple types, shared libraries serving different domain types, or compile-time guarantees that preserve an exact type relationship.
- Reasons to prefer simpler code. Distinguish cases where a plain interface or concrete code is clearer than generics, and apply the two-independent-call-sites rule of thumb before making a function generic.
- Backward-compatible generics adoption. Explain that generics uphold the Go 1 compatibility promise: existing code continues to work when upgrading to Go 1.18 or later. Describe how generic types and functions work alongside nongeneric code, allowing developers to introduce them incrementally rather than rewrite a codebase.
- Standard library generic utilities. Explain that Go’s standard library includes generic utilities for common data structures and algorithms, reducing the need to write them from scratch. Name the constraints, slices, maps, and cmp packages as the support this section will explore.
- Ordered type constraint. Explain that cmp.Ordered restricts a generic function to types that support ordering comparisons, including built-in integer, float, and string types.
- Generic collection utilities. Explain that the slices and maps packages provide reusable, type-safe operations without writing a separate function for each element type. Give examples such as sorting an ordered slice with slices.Sort and comparing maps with matching key and value types using maps.Equal.
- Domain collection operations. Describe how slices.SortFunc sorts a RecipeBox by serving size using a comparison function rather than requiring its elements to implement cmp.Ordered. Explain that maps.Keys can get keys from the GroceryList map type for a sorted list of ingredient names.
- Generic comparison with cmp. Explain that cmp.Compare compares values whose types support ordering and returns -1, 0, or 1 when the first value is less than, equal to, or greater than the second. Describe its use in generic sorting logic or other algorithms that need value comparisons.
- Generic reuse and type safety. Explain how type parameters and constraints let functions and collections work across types, reducing duplicated code while catching type errors at compile time.
- Practical limits of generics. Describe the need to weigh performance tradeoffs, language restrictions, and common pitfalls when using generics, despite their expressive power.
What it builds on
- Modeling a domain with types (chapter 5) · Introduces types as a way to model and abstract a domain.
- Slices (chapter 4) · The example filters slices.
- Pointer types (chapter 3) · Nodes link to the next node through pointers.
- Interfaces (chapter 5) · The generic Collection extends the idea of interfaces as contracts.
- Key and value types (chapter 4) · Introduces map key and value types.
- Maps (chapter 4) · Introduces built-in maps.
- Iterating over maps (chapter 4) · Introduces iteration over built-in maps.
Section by section
Introduction
Introduces Go generics as a type-safe layer of abstraction and previews the chapter’s progression from type parameters to reusable collections and standard-library utilities.
- Type-safe reuse through generics: Explain that Go generics use type parameters so functions, types, and interfaces can work with different data types without duplicating code or giving up compile-time type safety.
Why it matters: This is the reason to use the generic patterns developed throughout the chapter. - Chapter path through generics: Describe how the chapter starts with type parameters and develops generic collection types and utility functions, including lists, sets, and maps, before considering standard-library support.
Why it matters: The examples give a concrete setting for the chapter’s increasingly powerful generic patterns.
Key terms: generics, type parameters, compile-time type safety, generic collection types, utility functions, lists, sets, maps, standard library
6.1 Typed parameters in generics
This section introduces Go type parameters through a reusable filter and generic collections, showing how constraints preserve compile-time type safety.
- Reusable type-safe filtering: Explain how one generic Filter function replaces otherwise identical functions for recipes, steps, ingredients, and other types without giving up compile-time type safety.
Why it matters: The filter example shows the practical problem generics solve. - Type parameter syntax and inference: Describe the square-bracket type parameter syntax, such as node[T any], and explain that the compiler can infer T for a generic function from its arguments.
Why it matters: Type parameters are the notation that makes the section's reusable examples work. - Generic linked-list structure: Describe a linked list as nodes containing values and pointers to the next node, and explain why its node, list, and methods make it a useful example of generic types despite slices being simpler for most Go uses.
Why it matters: The linked list provides a concrete structure for following type parameters through several definitions. - Generic collection contracts and constraints: Explain how a generic Collection interface keeps operations consistent with its chosen element type, and distinguish the unrestricted any constraint from comparable, which permits equality operations.
Why it matters: Constraints let reusable collection code specify which element types support the operations it needs.
Key terms: Filter, generics, interface{}, type assertions, compile-time safety, type parameter, T, any, constraint, type inference
6.1.1 Using interface constraints
This subsection uses a Stringer-constrained linked list to show how an interface constraint restricts generic types at compile time.
- Stringer interface constraint: Explain that StringLinkedList accepts only element types that implement fmt.Stringer, so its elements can be used for string output and their String() method can be called confidently.
Why it matters: The constraint makes the list’s intended use enforceable before the program runs.
Key terms: StringLinkedList, fmt.Stringer, interface constraint, compile-time error, String()
6.1.2 Defining custom constraints
This section shows how custom interfaces can restrict generic code to selected types and explains why an exact type list excludes newly defined types with the same underlying type.
- Custom numeric constraints: Explain that a custom interface can constrain generic code to listed numeric types, such as int and float32, but a new type such as MyInt defined from int does not satisfy that constraint unless it is listed.
Why it matters: This distinction explains why a constraint can reject a type that appears to be numeric.
Key terms: custom interfaces, Numeric interface, constraint, underlying type
6.1.3 Using underlying types with tilde (~)
This section shows how tilde-based type sets let generic constraints accept custom types by underlying type, and how constraints can be combined into broader ones.
- Underlying type constraints: Explain how ~ changes a type-set constraint from matching an exact type to accepting any type with that underlying type. Contrast a constraint listing int and float32 with one listing ~int and ~float32, including whether MyInt, whose underlying type is int, qualifies.
Why it matters: This lets generic code support user-defined types without giving up compile-time type safety. - Composed ordering constraints: Describe the section’s example of building an Ordered constraint from Numeric by adding strings, allowing a generic type to express both numeric and alphabetic ordering.
Why it matters: Combining constraints shows how a reusable type set can become part of a broader one.
Key terms: tilde (~) operator, type sets, underlying type, Numeric, MyInt, Ordered, constraint hierarchy
6.1.4 Summary of type constraints
This subsection recaps how constraints control permitted types and shows how a simple generic Filter function works with recipe types.
- Choosing type constraints: Explain how built-in interfaces, custom type sets, and the tilde operator let generic collections and algorithms accept the intended types while preserving type safety.
Why it matters: The constraint expresses which types an operation is meant to support. - Filter constraint and type inference: Explain why Filter uses the any constraint and how a call with a slice of Recipe values obtains its type argument.
Why it matters: The example connects constraint choice to what a function actually does and shows why callers need not always write a type argument.
Key terms: built-in interfaces, custom type sets, tilde operator, type constraints, Filter, any, Recipe, type inference, T
6.2 Implementing generic functions
This section shows how generic functions create and operate on typed collections, including transforming elements from one type to another.
- Generic linked list construction: Explain how a generic constructor such as NewLinkedList uses a type parameter to return a pointer to a new LinkedList of that type, and why its comparable constraint limits which types can be used.
Why it matters: A generic constructor creates lists for different element types without giving up compile-time type checking. - Functions over generic collections: Describe how PrintCollection accepts a Collection[T] to work with collections of different element types, and how it checks whether a collection also implements List[T] before accessing elements by index.
Why it matters: This pattern lets a utility work across collection implementations while retaining a consistent element type. - Two type parameter transformations: Explain how a transformation function uses separate type parameters for its input and output types, so one function can handle conversions such as extracting string titles from Recipe values; describe how the compiler infers those types from the arguments.
Why it matters: Separate input and output types make a reusable conversion possible without writing a function for every type pair or relying on runtime interface{} casts.
Key terms: NewLinkedList, LinkedList[T], T comparable, PrintCollection, Collection[T], List[T], type assertion, RecipeBox, Recipe, string
6.3 Implementing generic interfaces
The section implements a generic list interface with a linked list, then uses that interface to write a type-safe list-reversal function.
- Generic list interface contract: Explain how List[T] defines operations on elements of type T and how LinkedList[T] implements that contract with methods, so callers can use the same interface with different element types.
Why it matters: The interface separates what a list can do from the data structure that performs those operations. - Linked list constraints and positions: Distinguish the interface's any constraint from LinkedList[T comparable], and describe how the implementation handles indexed access and changes. Explain why Get declares a zero value of T before it can return a value.
Why it matters: Constraints and positional methods determine which values this implementation accepts and how it fulfills the list contract. - Interface based list reversal: Describe how ReverseList takes a generic List[T] and reverses it in place by swapping elements in the first half with corresponding elements at the other end. Explain why using the interface makes the function reusable across list implementations and element types while retaining compile-time type safety.
Why it matters: ReverseList shows how an algorithm can depend on a generic interface rather than one concrete collection.
Key terms: List[T], LinkedList[T], type parameter, interface, any, comparable, node[T any], Get, Set, Insert
6.4 Multiple type parameters
This section uses a generic map to show how independent type parameters relate key and value types in reusable, type-safe abstractions.
- Independent key and value parameters: Explain how [K comparable, V any] gives a generic map separate key and value types, with a constraint on each: keys must support equality comparison, while values may be any type.
Why it matters: Separate constraints let one abstraction work with many key–value combinations without giving up type safety. - Generic map interface and implementation: Describe how HashMap[K, V] implements Map[K, V] by wrapping Go’s built-in map, and name the key operations and utilities the section implements.
Why it matters: The example shows how a generic interface can define behavior while a concrete type supplies storage. - Reusable map merging: Explain how MergeMaps uses the shared Map[K, V] interface to combine maps across key and value types, and why its key parameter must be comparable. State that the compiler can infer matching type arguments from the supplied maps at a call.
Why it matters: MergeMaps demonstrates that multiparameter generics can make an operation reusable across implementations, not just make a reusable container. - Map ordering and abstraction tradeoffs: Describe the ordering caveat of using a built-in map and the section’s advice for keeping multiparameter abstractions practical and readable.
Why it matters: These cautions help distinguish useful generic behavior from unnecessary complexity or an ordering guarantee the implementation does not provide.
Key terms: type parameters, K comparable, V any, constraints, Map[K, V], HashMap, Put, Get, Remove, ContainsKey
6.5 Limitations and considerations
This introduction frames Go generics as a tradeoff between reusable abstractions and clear, concrete code, then previews the considerations that guide their use.
- Generics adoption tradeoff: Explain that generics enable abstraction and code reuse, but adopting them requires weighing those benefits against the simplicity and clarity of concrete, type-specific Go code.
Why it matters: This tradeoff helps you decide when generics improve code rather than merely make it more abstract. - Generics considerations roadmap: Describe the considerations this section says it will cover: performance, language restrictions, common pitfalls, backward compatibility, and tooling and best practices.
Why it matters: These topics identify what to check before adopting generics widely.
Key terms: generics, concrete, type-specific code, abstraction, code reuse, simplicity, clarity, performance, language restrictions, common pitfalls, backward compatibility
6.5.1 Performance considerations
This section explains how Go compiles generic functions and how to assess and avoid performance costs in generic code.
- GC shape stenciling: Explain that Go generates a version of a generic function for each distinct GC shape, with pointer types sharing a version that may use an internal dictionary for method calls.
Why it matters: This explains why generic code does not have the same performance cost for every type argument. - Generic versus concrete speed: Distinguish the usually small performance gap between numeric generic code and handwritten concrete code, including why concrete code can still win a tight benchmark.
Why it matters: A small benchmark difference need not dictate the design of ordinary code. - Allocation risks and measurement: Describe how interface storage in generic code can cause heap escapes and GC pressure, and how narrow constraints, avoiding unnecessary conversions in hot loops, and profiling or benchmarking guide optimization.
Why it matters: Allocations and measured results are more useful guides than assuming generic code is slow.
Key terms: GC-shape stenciling, partial monomorphization, GC shape, stencil, internal dictionary, generic sum function, SumInt, inlining, interface overhead, heap
6.5.2 Limitations of Go generics
This section identifies two limits of Go generics: type parameters cannot define constants or struct field names, and operators require constraints that permit them.
- Constant and field restrictions: Describe why a type parameter cannot be used to declare a constant of a generic type or dynamically name a struct field.
Why it matters: These restrictions rule out some patterns available in languages with more flexible generics. - Operator constraints and type safety: Explain that arithmetic or comparison operators can be used on a type parameter only when its constraint permits those operations; for example, using + requires a constraint covering suitable numeric types. State that using + without such a constraint produces a compile-time error.
Why it matters: Explicit operator constraints let the compiler reject generic code that might apply an unsupported operation.
Key terms: type parameters, constants, struct field names, operators, type parameter, constraint, compile-time error, type safety
6.5.3 Common gotchas
This section identifies common Go generics pitfalls and explains how to recognize or avoid them.
- Constraint method sets: Explain that a type parameter constrained by an interface exposes only the methods in that interface’s method set, even when a concrete type has additional methods.
Why it matters: This prevents code from assuming that every method on a concrete type is available through a constrained type parameter. - Generic interface assertions: Distinguish a runtime type assertion to a matching generic instantiation from an assertion to an instantiation that cannot implement the interface, which the compiler rejects.
Why it matters: The distinction tells you whether a failed assertion is a runtime possibility or a compile-time error. - Type parameter naming hygiene: Describe how reusing type parameter names in nested scopes can cause confusion, and how distinct names, small focused abstractions, and thorough tests help avoid generic-code mistakes.
Why it matters: Clear names and incremental testing make subtle mistakes in generic code easier to catch. - Type inference surprises: Explain that Go may infer an unexpected type or fail to infer one for a generic use, and that explicit type parameters can clarify the intended type.
Why it matters: Explicit type arguments can make confusing inference behavior and error messages easier to resolve. - Reflection and runtime types: Explain why reflection cannot always recover a generic value’s original type arguments, and distinguish the dynamic type printed by %T for an interface value from the static type parameter T.
Why it matters: This avoids mistaking runtime type information for the type argument used in generic source code.
Key terms: interface constraint, method set, fmt.Stringer, type assertion, generic interfaces, type parameters, Collection[int], *LinkedList[int], *LinkedList[string], type parameter shadowing
6.5.4 When to use generics
This section gives practical criteria for deciding when Go generics justify their added complexity and when simpler code is clearer.
- Good uses for generics: Explain when generics earn their complexity: identical logic across multiple types, shared libraries serving different domain types, or compile-time guarantees that preserve an exact type relationship.
Why it matters: These cases show when a generic abstraction provides a benefit that concrete code may not. - Reasons to prefer simpler code: Distinguish cases where a plain interface or concrete code is clearer than generics, and apply the two-independent-call-sites rule of thumb before making a function generic.
Why it matters: Avoiding an unnecessary abstraction keeps Go code simpler and easier to read.
Key terms: generics, utility function, compile-time guarantees, type relationships, plain interface, concrete type, type arguments, independent call sites
6.5.5 Backward compatibility
This section explains that Go generics preserve existing code and can be adopted gradually.
- Backward-compatible generics adoption: Explain that generics uphold the Go 1 compatibility promise: existing code continues to work when upgrading to Go 1.18 or later. Describe how generic types and functions work alongside nongeneric code, allowing developers to introduce them incrementally rather than rewrite a codebase.
Why it matters: You can adopt generics where they help without changing everything else at once.
Key terms: Go 1 compatibility promise, backward compatibility, interoperable, type parameters
6.6 Standard library support
Introduces standard-library support for generics and previews the packages that provide reusable, type-safe utilities.
- Standard library generic utilities: Explain that Go’s standard library includes generic utilities for common data structures and algorithms, reducing the need to write them from scratch. Name the constraints, slices, maps, and cmp packages as the support this section will explore.
Why it matters: These packages show how to use generics in everyday Go code without reinventing common utilities.
Key terms: standard library, generics, constraints, slices, maps, cmp
6.6.1 The constraints package
This section explains which ordering constraint to use in generic Go code and how the choice depends on the Go version.
- Ordered type constraint: Explain that cmp.Ordered restricts a generic function to types that support ordering comparisons, including built-in integer, float, and string types.
Why it matters: The constraint lets a generic function accept only types for which ordering makes sense. - Version-specific constraint choice: State that new code targeting Go 1.21 or later should use standard-library cmp.Ordered, while code targeting Go 1.18 or 1.19 should use constraints.Ordered from golang.org/x/exp/constraints. Explain that their function signatures and behavior are identical; the import path differs.
Why it matters: Choosing the appropriate import can remove an external dependency without changing the generic function’s behavior.
Key terms: cmp.Ordered, type constraint, golang.org/x/exp/constraints, constraints.Ordered, Go 1.21
6.6.2 The slices and maps packages
Introduces the standard library’s generic slices and maps packages and shows how they support type-safe operations on ordinary collections and domain types.
- Generic collection utilities: Explain that the slices and maps packages provide reusable, type-safe operations without writing a separate function for each element type. Give examples such as sorting an ordered slice with slices.Sort and comparing maps with matching key and value types using maps.Equal.
Why it matters: These packages let you use common collection operations while retaining compile-time type checks. - Domain collection operations: Describe how slices.SortFunc sorts a RecipeBox by serving size using a comparison function rather than requiring its elements to implement cmp.Ordered. Explain that maps.Keys can get keys from the GroceryList map type for a sorted list of ingredient names.
Why it matters: Custom comparisons make generic utilities useful even when a domain type has no natural ordered value.
Key terms: slices, maps, slices.Sort, maps.Equal, type parameters, slices.SortFunc, RecipeBox, serving size, cmp.Ordered, maps.Keys
6.6.3 The cmp package
Introduces cmp.Compare as a standard-library way to compare ordered values in generic Go code.
- Generic comparison with cmp: Explain that cmp.Compare compares values whose types support ordering and returns -1, 0, or 1 when the first value is less than, equal to, or greater than the second. Describe its use in generic sorting logic or other algorithms that need value comparisons.
Why it matters: It lets you express comparisons in reusable, type-safe generic code.
Key terms: cmp package, cmp.Compare, ordering
6.7 Just getting started
This section recaps how Go generics support reusable, type-safe code while emphasizing their tradeoffs and Go’s preference for simplicity.
- Generic reuse and type safety: Explain how type parameters and constraints let functions and collections work across types, reducing duplicated code while catching type errors at compile time.
Why it matters: This is the main benefit the chapter wants you to carry into your own Go projects. - Practical limits of generics: Describe the need to weigh performance tradeoffs, language restrictions, and common pitfalls when using generics, despite their expressive power.
Why it matters: Using generics effectively means balancing reuse with Go’s emphasis on simple, clear code.
Key terms: generics, type parameters, constraints, generic collections, compile time, performance tradeoffs, language restrictions, common pitfalls, simplicity, clarity
6.8 Summary
This summary recaps how Go generics provide type-safe reuse through constrained type parameters, generic data structures, and standard-library support, while noting tradeoffs.
Where this is used later
- Benchmark testing (chapter 8) · Provides a reason to benchmark a generic implementation against its alternative
Concepts explained in this chapter
- genericsGeneric programming is a language feature that defines functions, types, and interfaces parameterized over types while preserving compile-time type safety.
- type constraintA type constraint restricts the types that may be substituted for a type parameter and specifies operations guaranteed to be valid for those types.
- comparablecomparable is a predeclared Go interface constraint satisfied by types whose values support equality and inequality comparisons.
- linked listA linked list is a sequence data structure whose elements are stored in nodes connected by links and whose element type can be parameterized in its generic form.
- generic interfaceA generic interface is an interface parameterized by one or more type parameters whose implementations must match the resulting type-specific contract.
- listA list is an ordered collection of elements that supports sequential access and typically dynamic size.
- interfaceAn interface is a contract specifying operations or type-set membership that a type can provide without defining the type's implementation.
- nodeA node is a data-structure element that stores a value and links to neighboring elements in a linked list.
- type safetyType safety is the property that prevents operations from applying values in ways inconsistent with their types.
- type assertionA type assertion is a Go operation that checks whether an interface value contains a specified concrete type and retrieves the contained value if it does.
- structThe struct keyword introduces a Go struct type declaration.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
