Go

Go Generics👨‍💻

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.

Key Takeaways

  • 1Type parameters are declared in square brackets after the function or type name: func Map[T, U any](slice []T, fn func(T) U) []U
  • 2Constraints are interfaces that specify what operations a type parameter supports -- any allows all types, comparable allows == and !=
  • 3Custom constraints use interface type elements (int | float64 | string) to restrict type parameters to specific concrete types
  • 4Type inference lets you omit type arguments when the compiler can deduce them from function arguments
  • 5Generic types (like Set[T comparable]) enable type-safe data structures that work across any type satisfying the constraint
  • 6Prefer interfaces for behavioral polymorphism and generics for type-safe data structures and algorithms -- they solve different problems

Master go generics

Take the Go Programming course with hands-on lessons and challenges.

Examples

Generic functions with type inference

go

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 type constraints with type elements

go

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.

Generic types -- a type-safe Set implementation

go

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.

Generic Result type for error handling

go

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.

When to use generics vs interfaces

go

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.

Common Mistakes

Mistake:

Using generics where a simple interface would suffice, overcomplicating the API signature

Fix:

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.

Mistake:

Forgetting the ~ prefix in type constraints, so named types with the right underlying type are excluded

Fix:

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.

Mistake:

Trying to use type assertions on type parameters inside a generic function

Fix:

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.

Mistake:

Creating deeply nested generic types like Map[K, Set[V]] that become unreadable and hard to instantiate

Fix:

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.

Best Practices

  • Start without generics. Write concrete implementations first. When you see duplication across types with identical logic, extract a generic function or type. Premature abstraction with generics is just as costly as premature abstraction with interfaces.
  • Use the standard library constraints: any for unconstrained types, comparable for map keys and equality checks, and cmp.Ordered (Go 1.21+) for sortable types. Avoid writing custom constraints unless the standard ones are insufficient.
  • Leverage type inference. If the compiler can deduce type arguments from function parameters, omit them. Write Filter(items, predicate) not Filter[string](items, predicate). Explicit type arguments are a code smell that suggests the function signature might be unclear.
  • Prefer generic functions over generic types. A standalone function like Map[T, U] is easier to understand and use than a generic Pipeline[T, U] struct. Generic types are best reserved for data structures (Set, Stack, Result) where the type parameter is fundamental to the type's identity.
  • Document constraints in the function's doc comment when the constraint name alone is not self-explanatory. A constraint like Number is clear, but Measurable[T, U] might need a sentence explaining what T and U represent.

Summary

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).

Practice Go with hands-on challenges

Learn go generics hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Go with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.