Go had resisted generics for years. The arguments against were practical: generics complicate the language, they interact badly with Go’s interface system, and most cases where you want generics can be handled with interface composition or code generation. The arguments weren’t wrong.
But the proposal that eventually shipped in Go 1.18 (2022) addressed a real gap — a gap that was producing either duplicated code or interface{} with runtime type assertions everywhere. Here’s what the proposal was solving.
What interface{} Actually Costs
The standard workaround for generic containers before generics: interface{} (or any in Go 1.18).
| |
Problems:
- No compile-time safety:
stack.Pop().(string)compiles, panics at runtime - Boxing overhead: every
int,float64,structpushed onto the stack gets boxed into aninterface{}— heap allocation, extra indirection - Unclear contract: the type signature says
interface{}— what does this stack actually hold?
The interface{} approach is fine for truly polymorphic code (a function that works with any value). It’s the wrong tool for containers and algorithms that are type-safe in every language that supports generics.
What Code Generation Costs
The alternative: code generation. Tools like go generate with templates can produce type-specific implementations:
| |
Code generation works but:
- Generated code is checked in or regenerated on every build (friction)
- The generator is another tool to maintain
- New types require re-running the generator
- Reading the generated code obscures the actual logic
The 2020 Proposal
The generics proposal by Ian Lance Taylor and Robert Griesemer introduced type parameters — a way to parameterise functions and types over types:
| |
The type parameter [T any] means “this type works for any type T.” The compiler generates appropriate code (or uses a single implementation with boxing depending on the calling context — an implementation detail).
Type Constraints
The any constraint means “any type at all.” More useful are constraints that restrict what types are allowed:
| |
Constraints are interfaces with a new ~T | ~U syntax for union types. The ~ means “underlying type” — ~int matches any type whose underlying type is int, including type UserID int.
What It Actually Enables
The headline use cases:
Generic data structures: type-safe maps, queues, trees, heaps without code generation or interface{}.
Generic utility functions: Map, Filter, Reduce that work for any type, with compile-time safety:
| |
Typed event buses, channels, results: instead of chan interface{}, use chan T with a type parameter.
What It Doesn’t Solve
Generics in Go are not Java generics. They don’t support method-level type parameters on types (only function-level), which limits certain patterns. They don’t support variance (no covariance/contravariance). The constraint system is simpler than Haskell typeclasses or Rust traits.
The Go team was deliberate about this. The goal was to handle the most common cases (containers, algorithms) cleanly, not to add the full expressiveness of a type-theoretical generics system.
For the cases generics don’t cover, the existing approaches (interface composition, any, code generation) remain appropriate. Generics are not a replacement for everything — they’re the right tool for a specific category of problems that was previously solved badly.