Go, End to End
Generics
Go got generics in 1.18, later than most languages with the feature, and the design shows some restraint that's worth understanding rather than just tolerating.
Type parameters
A generic function declares one or more type parameters in square brackets before its normal parameter list:
T here is constrained to int | float64, an inline union of the types the function is allowed to work with. Without a constraint, the compiler has no idea whether > is even defined for whatever T turns out to be, and generic code that doesn't compile for every possible T doesn't compile at all.
Constraints
Two built-in constraints cover most needs. any (an alias for interface{}) accepts literally anything, useful when your function doesn't actually operate on the values themselves (a generic Filter or Map function, say). comparable accepts any type that supports == and !=, which is what you need for things like a generic set or a function that checks for duplicates.
You can also write your own constraint as an interface listing the allowed types, and the ~ prefix matters: ~int means "int, or any named type whose underlying type is int", not just the literal int type. That distinction is the difference between a constraint that also accepts type UserID int and one that rejects it.
The constraints, slices, maps, and cmp packages
The standard library ships small packages of generic helpers built on top of this feature: slices (sorting, searching, comparing slices), maps (extracting keys/values, comparing maps), and cmp (an Ordered constraint and comparison helpers). Reach for these before writing your own generic sort or contains-check, they're maintained, tested, and idiomatic.
Where generics actually help
The honest case for generics in Go is narrow: container types (a generic Stack[T] or Set[T]), algorithms that operate identically regardless of element type (sort, filter, map, reduce over any slice), and utility functions that would otherwise be duplicated per type or written against interface{} with runtime type assertions scattered through the body.
Before generics, this duplication was genuinely painful:
func SumInts(items []int) int {
var total int
for _, item := range items {
total += item
}
return total
}
func SumFloats(items []float64) float64 {
var total float64
for _, item := range items {
total += item
}
return total
}
// ...and again for every other numeric type you need it forGenerics collapse that into the single Sum[T Number] shown above. That's the win: less duplication, not "generics because other languages have them."
When generics don't help
If what you actually need is polymorphism over BEHAVIOR (different types that do the same kind of thing but with different logic), that's what interfaces are for, and reaching for a type parameter there usually makes the code harder to read for no real benefit. A Shape interface with an Area() method is still the right tool for a collection of different shapes; making Area generic over shape types wouldn't simplify anything, it would just move a runtime decision into a compile-time one that didn't need to move.
Rule of thumb: if the function's logic is genuinely identical regardless of type, generics probably help. If different types need to behave differently, that's what interfaces already solve, and generics won't make it cleaner.
Try it yourself: write a generic Filter[T any](items []T, keep func(T) bool) []T function, then use it to filter a slice of ints for even numbers and a slice of strings for non-empty ones in the same program.