Go Programming

Go Generics Cheat Sheet: 10 Practical Code Patterns for Maps, Slices, and Custom Constraints

Since the release of Go 1.18, generics have transformed the language, allowing for type-safe, reusable code without the boilerplate of interface{} or reflection. For intermediate and advanced developers, mastering generics is no longer optional—it is essential for writing idiomatic, performant, and maintainable Go applications.

This cheat sheet distills 10 essential patterns that cover the most common use cases: manipulating slices, handling maps, and leveraging custom type constraints. Whether you are building a data processing pipeline or a complex API handler, these patterns will help you write cleaner Go code.

1. The Identity Constraint: Basic Type Parameters

The simplest form of a generic function uses the `any` alias (which is equivalent to `interface{}`) or a type parameter constrained to a specific set of types. This is ideal when you need to return a value of the same type as the input.

func firstElement[T any](s []T) (T, bool) {
    var zero T
    if len(s) == 0 {
        return zero, false
    }
    return s[0], true
}

2. Numeric Constraints for Mathematical Operations

Go's standard library provides built-in constraints like `~int` or `~float64`. The tilde (~) indicates that the underlying type must match the integer or float type, allowing you to work with custom integer types as well.

func SumNumbers[N int | float64](nums []N) N {
    var sum N
    for _, n := range nums {
        sum += n
    }
    return sum
}

3. Custom Constraints with Union Types

You can define custom interfaces to constrain type parameters to multiple distinct types. This is powerful for functions that need to accept both strings and integers.

type Number interface {
    int | int64 | float32 | float64
}

func Max[T Number](a, b T) T {
    if a > b {
        return a
    }
    return b
}

4. Generic Slice Filtering

Filtering is one of the most common operations on slices. A generic filter function accepts a predicate function that returns a boolean, allowing for highly reusable logic.

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

// Usage:
// evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })

5. Generic Map Merging

Merging maps is tricky with generics because of key constraints. You can create a generic function that merges two maps of the same key and value types.

func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
    merged := make(map[K]V)
    for _, m := range maps {
        for k, v := range m {
            merged[k] = v
        }
    }
    return merged
}

6. Distinct Values in Slices

To ensure uniqueness in a slice, you can leverage a map as a temporary set. This pattern requires the type parameter `T` to be comparable.

func Distinct[T comparable](items []T) []T {
    seen := make(map[T]bool)
    var result []T
    for _, item := range items {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

7. Generic Struct Mapping (Object Mapping)

When converting between different struct representations, generics allow you to map fields safely without reflection, provided you define the transformation logic externally or use helper libraries.

func MapStructs[T, U any](items []T, fn func(T) U) []U {
    result := make([]U, len(items))
    for i, item := range items {
        result[i] = fn(item)
    }
    return result
}

8. Safe Map Access with Defaults

Accessing map keys often requires checking existence to avoid zero values. A generic function can return a default value if the key is missing.

func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultVal V) V {
    if val, ok := m[key]; ok {
        return val
    }
    return defaultVal
}

9. Grouping Slices by Key

Grouping elements of a slice into buckets based on a key function is a classic functional programming pattern. This requires `K` to be comparable.

func GroupBy[K comparable, V any](items []V, keyFunc func(V) K) map[K][]V {
    groups := make(map[K][]V)
    for _, item := range items {
        key := keyFunc(item)
        groups[key] = append(groups[key], item)
    }
    return groups
}

10. Constrained Pointer Types

Sometimes you need to work with pointers to structs. You can constrain type parameters to pointers using interfaces or specific syntax, though this is less common than value types.

type PointerToStruct interface {
    *MyStruct
}

// Note: Direct pointer constraints are complex; usually, you constrain the underlying type
// and accept both pointers and values via separate functions or careful design.

Conclusion

Go generics are a powerful tool, but they require thoughtful application. The patterns above demonstrate how to handle common scenarios like filtering, mapping, and grouping while maintaining type safety. Remember that generics should simplify code, not complicate it. Start with these ten patterns, experiment with custom constraints, and you will quickly see the benefits in your Go projects.

Share: