Go 1.18 introduced generics through type parameters and constraints. Before generics, writing a function that worked across multiple types required either code duplication, code generation, or the empty interface with type assertions at runtime. Generics solve this with compile-time type safety: you write the function once, parameterize the types, and the compiler verifies everything statically.
The design is deliberately minimal compared to generics in Rust, C++, or even Java. Type parameters are constrained by interfaces. The built-in constraints any and comparable cover the most common cases. Custom constraints are just interfaces with type elements (union types like int | float64). There is no specialization, no variance annotations, and no higher-kinded types. This simplicity is intentional -- it covers 80% of use cases with 20% of the complexity.
The key question is not "how do generics work" but "when should you use them." The Go team's guidance is clear: start with concrete types, move to interfaces when you need polymorphism, and reach for generics only when interfaces force you into type assertions or code duplication. Generic data structures (sets, trees, caches), generic algorithms (map, filter, reduce), and type-safe utility functions are the sweet spot. Domain logic rarely benefits from generics.
Master go generics
Take the Go Programming course with hands-on lessons and challenges.
Map, Filter, and Reduce are classic generic functions. The any constraint allows them to work with any type. Type inference means you do not need to write Map[int, string](...) -- the compiler figures it out from the function arguments. These utilities are now available in the slices package of the standard library.
Custom constraints use type element syntax. The ~ prefix means "any type whose underlying type is int" -- this lets named types like Score satisfy the constraint. The Number constraint restricts to numeric types that support +. constraints.Ordered from the exp package covers all types that support < and >. These constraints enable type-safe numeric and comparison operations.
Set[T comparable] is a generic type parameterized by any comparable type. The comparable constraint is required because Go maps require comparable keys. NewSet uses variadic arguments for convenient initialization. All methods preserve type safety -- you cannot accidentally add a string to a Set[int]. This is the canonical example of where generics shine: type-safe data structures.
Result[T] is a generic wrapper that pairs a value with an error. The Then function chains Result-producing operations, short-circuiting on the first error. This demonstrates how generics enable functional patterns in Go. Note that this is not necessarily idiomatic Go -- the standard if err != nil pattern is preferred in most codebases -- but it shows the expressive power of generic types and functions.
Interfaces are for behavioral polymorphism: different types do different things through the same contract (Storer). Generics are for type-safe algorithms: the same logic applied to different types (Keys, Contains, Chunk). If you find yourself writing identical functions for []int and []string, use generics. If different types need different implementations of the same operation, use interfaces.
Using generics where a simple interface would suffice, overcomplicating the API signature
If the function only calls methods on the type parameter, an interface is simpler and more readable. func Process[T Processor](p T) is usually worse than func Process(p Processor). Reach for generics when you need type identity across parameters or return values -- for example, ensuring the input and output have the same type.
Forgetting the ~ prefix in type constraints, so named types with the right underlying type are excluded
type Addable interface { int | float64 } only matches the exact types int and float64. type Score int would NOT satisfy it. Use ~int | ~float64 to match any type whose underlying type is int or float64. The ~ is almost always what you want.
Trying to use type assertions on type parameters inside a generic function
Inside func Foo[T any](v T), you cannot write v.(string). Type parameters are not interface values -- they are concrete types known at compile time. If you need dynamic type dispatch, accept an interface (any) instead of using a type parameter, or restructure with a type switch on an any parameter.
Creating deeply nested generic types like Map[K, Set[V]] that become unreadable and hard to instantiate
Keep generic type nesting shallow. If the type signature is hard to read, introduce a type alias or break the composition into named types. Readability is a feature in Go -- a generic type should be easier to understand than the code duplication it replaces.
Go generics provide compile-time type safety for algorithms and data structures that work across multiple types. Type parameters are constrained by interfaces, with any and comparable covering most cases. Custom constraints use type element syntax with the ~ prefix for underlying type matching. Type inference keeps call sites clean. The golden rule: use interfaces for behavioral polymorphism (different types, different logic) and generics for type-safe algorithms (same logic, different types).
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.