Generics landed in Go 1.18 back in early 2022, and after the initial flurry of everyone rewriting their linked-list library to prove a point, the dust has settled. So here's the honest question I've been sitting with: in day-to-day Go, do I actually reach for them? Mostly no. Sometimes yes, and when yes, gratefully.
Let me try to be specific rather than vibesy about it, because "use them where they help" is true and useless.
Where they genuinely earn their place
The cleanest win is the small toolbox of container and collection helpers that you used to copy-paste with the type swapped out. Map, Filter, Keys, Values, a generic Set, a bounded LRU cache. Before generics these were either written N times, once per type, or smeared through interface{} with a type assertion and a runtime panic waiting to happen. Now they're written once, type-safe, and the compiler does the checking.
func Map[T, U any](in []T, f func(T) U) []U {
out := make([]U, len(in))
for i, v := range in {
out[i] = f(v)
}
return out
}
That's the canonical example for a reason. It's obviously correct, it's reusable across every slice type you'll ever have, and there is genuinely no non-awful way to write it without generics. The standard library agrees: slices and maps arrived precisely because these helpers wanted to exist and couldn't before.
The other clear win is anything that's parameterised over a numeric or ordered type. A Min, a Max, a Clamp. The constraints machinery and cmp.Ordered make these pleasant in a way they simply weren't when you had to pick int and then write the float64 version next to it.
There's a quieter win too, which is type-safe wrappers around things that were stringly-typed before. A generic Optional[T], a Result[T], a typed channel helper, a worker pool that takes a func(T) (U, error) and gives you back a stream of U. These aren't glamorous, but they remove a whole category of "I forgot to assert and it panicked at runtime" bugs by moving the check to compile time, which is precisely where you want it.
Where I deliberately don't
Here's the part the early enthusiasm got wrong. Most application code is not a container library. It's business logic, and business logic is usually concrete. It deals with an Order, a User, an Invoice. There's exactly one type. Making it generic doesn't make it reusable, it makes it harder to read in exchange for a flexibility you will never use.
I've seen, and I'll admit written, the generic repository:
type Repository[T any] interface {
Get(ctx context.Context, id string) (T, error)
Save(ctx context.Context, entity T) error
}
It looks tidy. It feels clever. And then the first time OrderRepository needs a method that UserRepository doesn't, the abstraction springs a leak, you start adding type parameters and constraints to paper over it, and six months later it's harder to follow than two honest concrete structs would have been. A bit of duplication you can read beats a clever abstraction you have to decode. I'll take the copy-paste.
The same goes for the temptation to make a function generic "just in case it's reused". Go has a strong cultural bias towards writing the concrete thing first and extracting the abstraction only once you have two or three real call sites that actually share it. Generics don't change that advice, they just give you a sharper tool for the extraction when the day comes. Reaching for a type parameter before you have the second caller is speculative generality with a new coat of paint, and the compiler can't save you from a design that was wrong to begin with.
There's a performance footnote here as well, because someone always asks. Go's generics aren't free at runtime in the way people sometimes assume. The implementation uses a technique called dictionaries and stenciling, and for some shapes of code it can box values or go through an indirection that a hand-written concrete version wouldn't. In practice this almost never matters for application code, the difference is in the noise next to a database call. But it's a reason not to reach for generics in a genuinely hot loop without measuring, and a reason the "make everything generic" instinct is wrong on more than just readability grounds.
The bit that's easy to forget
Generics in Go aren't C++ templates and they aren't Java erasure, they're their own thing, and the constraint system is where it shows. Most of the time you want any or comparable or cmp.Ordered and you're done. The moment you find yourself writing a bespoke constraint interface with a union of a dozen approximated types, stop and ask whether an ordinary interface with a method would say the same thing more clearly.
Because that's the real comparison, and it gets lost. Generics and interfaces solve overlapping problems. An interface says "I don't care what you are, as long as you can do this". A type parameter says "I care that you're all the same concrete thing, and I'll preserve that". If your code only needs behaviour, an interface is usually simpler and reads better. If your code needs to carry the concrete type through without boxing or assertions, that's the generics-shaped hole.
// interface: I just need something I can write to
func writeAll(w io.Writer, chunks [][]byte) error
// generic: I need to return the same concrete type I was given
func First[T any](items []T) (T, bool)
writeAll has no business being generic. First has no business being an interface.
So, do I need them
For application code, rarely. I write services for a living, and the bulk of what I write is concrete types doing concrete things, where generics would be a tax I pay for nothing.
For the thin layer of utilities underneath all that, yes, and they've quietly made that layer better. My little internal/xslices and internal/xmaps packages are smaller, safer and less copy-pasted than their pre-1.18 ancestors, and I never think about them, which is exactly the point.
The honest answer is that generics are a good feature that's mostly a library author's feature, and most of us are application authors most of the time. Use them where you're genuinely writing something reusable over types. Resist them everywhere you're tempted to make the simple thing look impressive. Two years in, that's a boring conclusion, and boring is usually where Go is at its best.