Generics let us write one function or type that works for many types, instead of copy-pasting the same logic for int, string, float64, and so on.
They landed in Go 1.18 — a huge addition the community had wanted for over a decade.
In simple language, generics let us say “this function works for any type T, and the compiler fills in the real type at compile time.”
The problem they solve
Before generics, a Max function needed one copy per type. Painful.
func MaxInt(a, b int) int { if a > b { return a }; return b }
func MaxFloat(a, b float64) float64 { if a > b { return a }; return b }
// ...and so on, forever
With generics it’s one function:
import "golang.org/x/exp/constraints"
// T is a type parameter, constrained to ordered types (things we can compare with >)
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(3, 5)) // works with int
fmt.Println(Max("a", "b")) // works with string
fmt.Println(Max(1.5, 0.5)) // works with float64
}
The [T constraints.Ordered] part is the type parameter list. T is a placeholder, and constraints.Ordered says “T must be a type that supports <, >, etc.” The compiler usually infers T from the arguments, so we just call Max(3, 5) normally.
Constraints — the rules on T
A constraint says what a type parameter is allowed to be. We can’t just do a > b on any type — only on comparable/ordered ones. Constraints enforce that. There are a few sources:
comparable— a built-in constraint for types that support==and!=(needed for map keys, equality checks).- The
constraintspackage (golang.org/x/exp/constraints) — ready-made ones likeOrdered,Integer,Float,Signed. any— the loosest constraint, an alias forinterface{}. Means “literally any type.”- Custom interface constraints — we write our own.
// a custom constraint: T must be one of these number types
type Number interface {
~int | ~int64 | ~float64 // | means "union", ~ means "any type based on this"
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
The ~int (with a tilde) means “int or any type whose underlying type is int” — so a custom type Celsius int still qualifies. Without the ~ it would only match int exactly.
Generic types, not just functions
Structs can be generic too. A type-safe stack that holds any type:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) }
func (s *Stack[T]) Pop() T {
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last
}
// usage — we specify the type once: Stack[string]
var s Stack[string]
s.Push("hi")
Now Stack[int] and Stack[string] are separate, fully type-checked types. No interface{} casting, no runtime surprises.
Generics vs interfaces vs any — when to use what
This is the real interview question. Here’s the mental model:
- Generics — when the same algorithm runs over different types and we want to keep static type safety. The function body doesn’t care what
Tis. - Interfaces — when different types provide different behavior behind a shared method set. The whole point is polymorphism (different code per type).
any— when we genuinely don’t care about the type and accept losing type safety (and paying for runtime type assertions).
The community caution: don’t overuse them
The Go team’s own advice: reach for generics last, not first. They make code harder to read and were deliberately kept minimal.
In simple language — if a plain interface or a concrete type does the job, use that. Generics earn their place only when we’d otherwise copy-paste the exact same logic for several types (containers, and utilities like Map/Filter/Max). Sprinkling [T any] everywhere is a code smell.
Interview soundbite
Generics arrived in Go 1.18 and let us write one function or type parameterized over types, like func Max[T constraints.Ordered](a, b T) T. Constraints (comparable, the constraints package, or custom interface unions with ~ and |) restrict what T can be. Use generics when the same logic spans many types, interfaces when types share different behavior, and any only as a last resort. The Go team’s guidance is explicit: don’t overuse them.