What are Go generics, and what problem did they solve?
Quick Answer
Generics, added in Go 1.18, let you write functions and types parameterized over a type, using square-bracket type parameters: func Map[T, U any](s []T, f func(T) U) []U. Before generics, writing a function or container that worked across multiple types meant either duplicating code for each concrete type, or falling back to interface{}/any and giving up compile-time type safety along with the runtime cost of boxing and type assertions. Generics let you write one implementation of something like a Stack[T], Map, Filter, or Min/Max function that works for int, string, or any custom type, fully type-checked at compile time, with no runtime type assertions needed at the call site.
Detailed Answer
Generics were the single most requested Go feature for years, precisely because the pre-1.18 alternatives were both unsatisfying.
Before generics: duplication or any
func SumInts(nums []int) int {
var total int
for _, n := range nums {
total += n
}
return total
}
func SumFloats(nums []float64) float64 {
var total float64
for _, n := range nums {
total += n
}
return total
}
// ...repeat for every numeric type you need...
With generics: one implementation, type-checked
type Number interface {
int | int64 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4.0
T is a type parameter, constrained to only the types listed in the Number interface. The compiler generates type-safe code for each concrete type used, and catches a mismatched call (Sum([]string{...})) at compile time.
A generic container
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
n := len(s.items) - 1
v := s.items[n]
s.items = s.items[:n]
return v, true
}
What problem this actually solved
Before 1.18, Go's standard answer to "I need this to work for any type" was interface{}, which pushed every type error to runtime and required manual assertions everywhere. Generics move that type-checking back to compile time, without giving up Go's simplicity elsewhere in the language, and without needing separate hand-written code per concrete type.