Idiomatic Generics

Overview

Generics are powerful but should be used judiciously. This chapter covers when to use generics vs interfaces, and best practices.

When to Use Generics

Good uses: - Container types (Stack, Set, Queue) - Utility functions (Map, Filter, Reduce) - Type-safe concurrent constructs - Avoiding reflection

Don’t use when: - Interface works fine - Only one type will ever be used - Adding complexity without benefit

Generics vs Interfaces

Use Interface When:

// Behavior-based polymorphism
type Writer interface {
    Write([]byte) (int, error)
}

func Save(w Writer, data []byte) error {
    _, err := w.Write(data)
    return err
}

Use Generics When:

// Type preservation needed
func Reverse[T any](s []T) []T {
    result := make([]T, len(s))
    for i, v := range s {
        result[len(s)-1-i] = v
    }
    return result
}

Guidelines

1. Start with Concrete Types

// Start here
func ReverseInts(s []int) []int { }

// Generalize only when needed
func Reverse[T any](s []T) []T { }

2. Use Meaningful Constraints

// Too broad
func Process[T any](v T) { }

// Better: documents requirements
func Process[T constraints.Ordered](v T) { }

3. Don’t Over-Constrain

// Over-constrained
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

// Better: use existing constraint
func Sum[T constraints.Integer | constraints.Float](nums []T) T

4. Type Inference is Your Friend

// Let Go infer when obvious
result := Map(nums, double)

// Explicit only when needed
result := Convert[int, float64](nums)

Common Patterns

slices Package (stdlib)

import "slices"

slices.Sort(nums)
slices.Reverse(nums)
slices.Contains(nums, 5)
slices.Index(nums, 5)

maps Package (stdlib)

import "maps"

maps.Clone(m)
maps.Keys(m)
maps.Values(m)

Summary

Decision Choice
Need type preservation Generics
Need polymorphic behavior Interface
Operating on containers Generics
Method set requirements Interface