Quidest?

Go Generics

Generics in Go

Generics were introduced in Go 1.18 (released March 2022) and represent one of the most significant additions to the language since its inception.

Core Concepts

Type Parameters

Type parameters allow functions and types to work with any type that satisfies certain constraints:

1func Print[T any](value T) {
2    fmt.Println(value)
3}
4
5// Usage
6Print[int](42)
7Print[string]("hello")
8Print(3.14) // Type inference: T inferred as float64

Type Constraints

Constraints specify what operations are permitted on a type parameter. They are defined using interfaces:

 1// Built-in constraint: any (alias for interface{})
 2func Identity[T any](v T) T {
 3    return v
 4}
 5
 6// Built-in constraint: comparable (supports == and !=)
 7func Contains[T comparable](slice []T, target T) bool {
 8    for _, v := range slice {
 9        if v == target {
10            return true
11        }
12    }
13    return false
14}

Custom Constraints

You can define your own constraints using interface syntax:

 1// Method-based constraint
 2type Stringer interface {
 3    String() string
 4}
 5
 6func Stringify[T Stringer](v T) string {
 7    return v.String()
 8}
 9
10// Type set constraint (union of types)
11type Number interface {
12    int | int8 | int16 | int32 | int64 |
13    uint | uint8 | uint16 | uint32 | uint64 |
14    float32 | float64
15}
16
17func Sum[T Number](values []T) T {
18    var total T
19    for _, v := range values {
20        total += v
21    }
22    return total
23}

The ~ Operator (Underlying Type)

The tilde operator matches types with the same underlying type:

 1type MyInt int
 2
 3type Integer interface {
 4    ~int | ~int64  // Matches int, int64, AND any type with int or int64 as underlying type
 5}
 6
 7func Double[T Integer](v T) T {
 8    return v * 2
 9}
10
11var x MyInt = 5
12Double(x) // Works because MyInt's underlying type is int

Combining Constraints

You can combine method requirements with type sets:

1type OrderedStringer interface {
2    ~int | ~string
3    String() string
4}

Generic Types

Generic Structs

 1type Stack[T any] struct {
 2    items []T
 3}
 4
 5func (s *Stack[T]) Push(item T) {
 6    s.items = append(s.items, item)
 7}
 8
 9func (s *Stack[T]) Pop() (T, bool) {
10    if len(s.items) == 0 {
11        var zero T
12        return zero, false
13    }
14    item := s.items[len(s.items)-1]
15    s.items = s.items[:len(s.items)-1]
16    return item, true
17}
18
19// Usage
20intStack := Stack[int]{}
21intStack.Push(1)
22intStack.Push(2)

Generic Maps and Slices

 1type Set[T comparable] map[T]struct{}
 2
 3func NewSet[T comparable]() Set[T] {
 4    return make(Set[T])
 5}
 6
 7func (s Set[T]) Add(v T) {
 8    s[v] = struct{}{}
 9}
10
11func (s Set[T]) Contains(v T) bool {
12    _, ok := s[v]
13    return ok
14}

The constraints Package

The golang.org/x/exp/constraints package provides useful predefined constraints:

 1import "golang.org/x/exp/constraints"
 2
 3// constraints.Ordered: types that support < > <= >=
 4func Max[T constraints.Ordered](a, b T) T {
 5    if a > b {
 6        return a
 7    }
 8    return b
 9}
10
11// constraints.Integer: all integer types
12// constraints.Float: all float types
13// constraints.Complex: all complex types
14// constraints.Signed: all signed integer types
15// constraints.Unsigned: all unsigned integer types

Multiple Type Parameters

 1type Pair[K comparable, V any] struct {
 2    Key   K
 3    Value V
 4}
 5
 6func NewPair[K comparable, V any](k K, v V) Pair[K, V] {
 7    return Pair[K, V]{Key: k, Value: v}
 8}
 9
10// Map function with two type parameters
11func Map[T, U any](slice []T, f func(T) U) []U {
12    result := make([]U, len(slice))
13    for i, v := range slice {
14        result[i] = f(v)
15    }
16    return result
17}
18
19// Usage
20nums := []int{1, 2, 3}
21strs := Map(nums, func(n int) string {
22    return fmt.Sprintf("%d", n)
23})

Type Inference

Go can often infer type parameters from arguments:

1func First[T any](slice []T) T {
2    return slice[0]
3}
4
5// Explicit
6result := First[int]([]int{1, 2, 3})
7
8// Inferred (preferred when unambiguous)
9result := First([]int{1, 2, 3})

Limitations and Gotchas

1. No Method Type Parameters

Methods cannot have their own type parameters (only the receiver can be generic):

1type Container[T any] struct { value T }
2
3// ❌ Invalid: methods cannot have additional type parameters
4// func (c Container[T]) Convert[U any]() U { ... }
5
6// ✅ Valid: use a standalone function instead
7func Convert[T, U any](c Container[T], f func(T) U) U {
8    return f(c.value)
9}

2. No Specialization

You cannot provide specialized implementations for specific types:

1// ❌ Not possible in Go
2func Print[T any](v T) { fmt.Println(v) }
3func Print[int](v int) { fmt.Printf("Integer: %d\n", v) } // Cannot specialize

3. Zero Values

Getting the zero value of a generic type:

1func Zero[T any]() T {
2    var zero T
3    return zero
4}
5
6// Or using *new(T)
7func Zero2[T any]() T {
8    return *new(T)
9}

4. Type Assertions with Generics

 1func Process[T any](v T) {
 2    // Type switch works
 3    switch val := any(v).(type) {
 4    case int:
 5        fmt.Println("int:", val)
 6    case string:
 7        fmt.Println("string:", val)
 8    default:
 9        fmt.Println("other:", val)
10    }
11}

5. Pointer Constraints

A common pattern for methods that require a pointer receiver:

 1type Setter interface {
 2    Set(string)
 3}
 4
 5// Constraint: *T must implement Setter
 6func SetAll[T any, PT interface { *T; Setter }](items []T, value string) {
 7    for i := range items {
 8        PT(&items[i]).Set(value)
 9    }
10}

Practical Patterns

Generic Result Type

 1type Result[T any] struct {
 2    Value T
 3    Err   error
 4}
 5
 6func Ok[T any](v T) Result[T] {
 7    return Result[T]{Value: v}
 8}
 9
10func Err[T any](err error) Result[T] {
11    return Result[T]{Err: err}
12}

Generic Optional Type

 1type Optional[T any] struct {
 2    value *T
 3}
 4
 5func Some[T any](v T) Optional[T] {
 6    return Optional[T]{value: &v}
 7}
 8
 9func None[T any]() Optional[T] {
10    return Optional[T]{}
11}
12
13func (o Optional[T]) IsSome() bool {
14    return o.value != nil
15}
16
17func (o Optional[T]) Unwrap() T {
18    if o.value == nil {
19        panic("called Unwrap on None")
20    }
21    return *o.value
22}

Generic Cache

 1type Cache[K comparable, V any] struct {
 2    mu    sync.RWMutex
 3    items map[K]V
 4}
 5
 6func NewCache[K comparable, V any]() *Cache[K, V] {
 7    return &Cache[K, V]{items: make(map[K]V)}
 8}
 9
10func (c *Cache[K, V]) Get(key K) (V, bool) {
11    c.mu.RLock()
12    defer c.mu.RUnlock()
13    v, ok := c.items[key]
14    return v, ok
15}
16
17func (c *Cache[K, V]) Set(key K, value V) {
18    c.mu.Lock()
19    defer c.mu.Unlock()
20    c.items[key] = value
21}

Performance Considerations

Go generics use a hybrid implementation:

This means:

#golang #programming #generics #syntax #coding #software #engineering #computer