Generics have been in Go for a few years now, long enough that the initial excitement has faded and we can be honest about how often we actually reach for them. My honest answer, having written a fair amount of Go since they landed, is: less than I expected, and almost always for the same two or three things. The rest of the time the old, slightly boring way is still the right way, and I have stopped feeling clever about avoiding it.
The thing nobody tells you when generics arrive in a language is that the hard part is not learning the syntax. It is resisting the urge to use them everywhere now that you can. There is a phase, and I went through it, where every function with an interface{} in it looks like a personal failing waiting to be fixed with a type parameter.
Where they actually earn it
The clearest win, and the one the standard library blessed almost immediately, is small generic helpers over slices and maps. Map, Filter, Keys, Values, the sort of thing you used to write five times with five different element types. Now you write it once:
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
That is genuinely nicer than the alternatives. It is type-safe, it is obvious, and it deletes a category of copy-paste from the codebase. The slices and maps packages in the standard library cover most of what I used to hand-roll, and reaching for them is a clear improvement rather than a clever flourish.
The other place they earn their keep is constraints on numeric code. If you have a function that genuinely should work over any integer type, a constraint expressing exactly that is cleaner and safer than the interface gymnastics we used to do. It is a narrow case, but when it fits, it fits well.
Where they are a trap
The trap is using a type parameter where an interface would have said what you meant. Go's interfaces are about behaviour: this thing has a Read method, I do not care what it is. Generics are about types: this thing is some T, and I will work with whatever T turns out to be. When you find yourself adding constraints just to call methods, you have usually talked yourself out of an interface that would have read better and compiled faster.
I keep a rough rule in my head. If the abstraction is "anything that can do X", that is an interface. If the abstraction is "a container of some type, and I want to preserve that type through the call", that is a generic. The second case is real but rare. Most of my code is the first case, and most of the time the honest answer to "do I need generics here" is no.
So, do I need them? Sometimes, and when I do they are a genuine relief rather than a workaround. But the day-to-day Go I write looks much like the Go I wrote before they existed: plain structs, small interfaces, functions that do one thing. Generics turned out to be a tool for a specific shelf, not a new way of building the whole house, and Go is better for the language designers having held that line so firmly. The temptation to overuse them was the real risk, and that one is on us, not on the feature.