3 min read
On this page

Generics in Go

Go 1.18 introduced generics, adding type parameters to functions and types. Generics let you write code that works with multiple types without sacrificing type safety. They are powerful but should be used judiciously — most Go application code does not need them.

Type Parameters

A generic function declares type parameters in square brackets before the regular parameters.

func Map[T any, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    doubled := Map(numbers, func(n int) int { return n * 2 })
    fmt.Println(doubled)

    words := []string{"hello", "world"}
    lengths := Map(words, func(s string) int { return len(s) })
    fmt.Println(lengths)
}
[2 4 6 8 10]
[5 5]

The compiler infers the type arguments from the function arguments, so you rarely need to specify them explicitly.

Constraints

Type parameters need constraints that describe what operations are allowed on the type. The simplest constraint is any, which permits all types but allows no operations beyond assignment and comparison with nil.

// any: no operations on T beyond passing it around
func Identity[T any](v T) T {
    return v
}

The comparable constraint allows types that support == and !=, which is required for map keys.

func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

func main() {
    fmt.Println(Contains([]int{1, 2, 3}, 2))
    fmt.Println(Contains([]string{"a", "b"}, "c"))
}
true
false

Custom Constraints

You define custom constraints as interfaces with type elements.

type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~float32 | ~float64
}

func Sum[T Number](values []T) T {
    var total T
    for _, v := range values {
        total += v
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3}))
    fmt.Println(Sum([]float64{1.5, 2.5, 3.0}))
}
6
7

The tilde ~ means "any type whose underlying type is." Without it, only the exact named type matches. With ~int, a custom type like type UserID int also satisfies the constraint.

The constraints & cmp Packages

The golang.org/x/exp/constraints package provides common constraint definitions, and Go 1.21 added cmp to the standard library.

import "cmp"

func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Max(3, 7))
    fmt.Println(Max("alpha", "beta"))
}
7
beta

cmp.Ordered covers all types that support the <, >, <=, >= operators — integers, floats, and strings.

Generic Types

You can define generic data structures, not just functions.

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) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    last := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return last, true
}

func main() {
    var s Stack[int]
    s.Push(10)
    s.Push(20)
    v, ok := s.Pop()
    fmt.Println(v, ok)
}
20 true

When to Use Generics

Generics shine in specific situations.

Data structures. A linked list, tree, or stack that should work with any element type.

type Pair[A, B any] struct {
    First  A
    Second B
}

Algorithms. Sorting, filtering, mapping, reducing — operations defined by behavior, not by a specific type.

func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

Utility functions. Functions like Keys, Values, or GroupBy that operate on maps or slices generically.

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
}

When Not to Use Generics

Most Go application code does not need generics. Avoid them when:

  • A concrete type works fine. If your function only ever operates on []string, just use []string.
  • An interface already solves the problem. io.Reader does not need generics.
  • The generic version is harder to read than two concrete implementations.
  • You are writing business logic. Domain types are usually specific, not generic.

The Go proverb applies: a little copying is better than a little dependency. Two simple concrete functions are often clearer than one generic function with constraints.

A Real-World Example: Result Type

A generic result type can clean up functions that return a value or an error.

type Result[T any] struct {
    Value T
    Err   error
}

func NewResult[T any](val T, err error) Result[T] {
    return Result[T]{Value: val, Err: err}
}

func (r Result[T]) Unwrap() (T, error) {
    return r.Value, r.Err
}

func FetchUser(id int) Result[User] {
    user, err := db.FindUser(id)
    return NewResult(user, err)
}

This pattern is more useful in library code than in typical application code, where the standard (T, error) return convention works well.

Common Pitfalls

  • Overusing generics. Writing generic code when a concrete type would be simpler and more readable. Start concrete, generalize only when you have a clear need.
  • Complex constraint hierarchies. Deeply nested or overly clever constraints make code hard to understand. Keep constraints as simple as possible.
  • Forgetting the zero value. When a generic function needs to return a "nothing" value, you must declare var zero T and return it — you cannot return nil for non-pointer type parameters.
  • No method type parameters. Go does not allow type parameters on methods (only on functions and type definitions). This limits some patterns common in other languages.
  • Type inference limits. The compiler can usually infer type arguments, but sometimes you need to specify them explicitly, especially with complex nested generics.

Key Takeaways

  • Go 1.18 introduced generics with type parameters in square brackets.
  • Constraints define what operations a type parameter supports: any, comparable, cmp.Ordered, or custom interface constraints.
  • Use the ~ prefix in constraints to match types with a given underlying type.
  • Generics are best for data structures, algorithms, and utility functions.
  • Most application code should use concrete types or interfaces instead of generics.
  • Keep generic code simple — if the generic version is harder to read than two concrete versions, use the concrete versions.