What are Go generics, and what problem did they solve?

5 minintermediategogenericstype-parameters

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.