Generics in Go: three years later, what has survived
Actualizado: 2026-05-03
When Go 1.18 added generics in March 2022, the community split into two camps. Some had been asking for them for years and welcomed them as the missing step to modernize the language. Others suspected they would corrupt the simplicity that made Go attractive. Both had a point, and three years later we have enough perspective to judge without the passion of the initial moment.
What’s most interesting is that the outcome hasn’t quite gone either way. Generics in Go haven’t exploded into mass usage, but they also haven’t been an ignored hot potato. They occupy a very specific place in the language, and understanding what that place is says a lot about how community thinking has evolved.
Key takeaways
- Generics did not replace
interface{}at the surface of idiomatic code — the most common prediction was wrong. - Where they’ve rooted strongly is in low-level libraries: collections, concurrency primitives, database clients.
- The
sliceslibrary (promoted to standard in Go 1.21) is the most tangible daily use case. - Generic methods on non-generic receivers still aren’t allowed — the community replaces them with free functions.
- The rule that held up best: write the concrete function first; generalise only if you copy the same logic for three different types.
What hasn’t happened
The most common prediction at first was that generics would replace interface{} in all contexts where it was used. That hasn’t really happened. If you look at idiomatic code in the most active Go projects — Kubernetes, Docker, CockroachDB, the standard library itself — the public surface is still dominated by traditional interfaces, concrete function signatures, and use of any where flexibility is genuinely needed.
The main reason is that generics, in the form finally approved for Go, have design constraints that make them less universal than C++ templates or Rust trait bounds. That’s a design virtue, because it keeps compile times reasonable and avoids incomprehensible error messages, but it comes at an expressiveness cost.
The second reason is cultural. Go’s philosophy has always preferred concretion over abstraction, and the community has internalized that enough that the default reflex is “write the concrete function first, generalise only if clearly needed”.
What has happened
Where generics have rooted strongly is in the deep layers of libraries. Open code from golang.org/x/exp/slices, from recent sync utilities, or from database clients like pgx, and you’ll find generics everywhere. That’s the kind of code where generalisation makes sense: fundamental operations on collections, concurrency utilities, deserializers.
The pattern that has emerged is clear. Generic functions are used when three conditions are met:
- The logic is truly identical across types, not just similar.
- There’s performance to be lost with
anyand reflection, and the cost is noticeable. - The generic signature is clearer for the user than an interface.
The slices case is illustrative. This library was experimentally available for some time and was promoted to standard in Go 1.21. It offers functions like slices.Contains, slices.Index, slices.Sort, implemented with generics. Anyone who wrote Go before 2022 had reimplemented these functions over and over for each type, because there was no reasonable alternative. Now they’re one line.
What has surprised
One effect not anticipated is the impact on database clients. sqlc and especially pgx version 5 offer type-safe row scanning into structs via generics. Before, this was done with reflection and a good amount of ceremony; now you have a pgx.CollectRows[T] function that returns a []T directly, and the compiler tells you if the struct doesn’t match the columns.
This pattern has spread to JSON serialization, config parsing, and several testing frameworks.
The other place generics have taken unexpected hold is typed channels and concurrency primitives. Libraries like sourcegraph/conc offer worker pools, result groups, and similar primitives with generics, and the experience is qualitatively better than the equivalent with interface{} and type assertions.
What didn’t take off
The big exception is what some expected to be the star case: complex generic data types, like balanced trees, typed heaps, graphs. These exist in the ecosystem, but adoption has been moderate. For modestly sized collections, native slices and maps are enough.
Generic methods are another story. Go doesn’t allow declaring generic methods on a non-generic receiver. This limits patterns like “a Map method on a collection” that other languages allow. The community has learned to live with that via free functions (Map(coll, fn) instead of coll.Map(fn)).
Looking ahead
Three years in, my read is that Go has absorbed generics without losing identity. They’ve gone into the place where they add real value (low-level libraries, collection utilities, data clients) and stayed out of places where they would have added noise (everyday business code, public service APIs).
What’s coming, based on proposals being debated, is loosening specific cases: allowing more type inference where today annotations are needed, and perhaps allowing generic methods in limited circumstances.
Conclusion
If someone asks whether they should start using generics in their Go code, my answer is still the one from three years ago: first write the concrete function. If you then find yourself copying it for three different types and the logic is identical, then yes, generalise. That modest criterion is what’s held up best.
Generics in Go are a library infrastructure tool, not an application code tool. Understanding that specific place — no bigger, no smaller — is what makes their adoption successful.