Generics in Go

Type Theory & Generics Fundamentals

What are Generics?

Generics enable writing code that works with multiple types while maintaining type safety at compile time. They provide parametric polymorphism, allowing functions and types to be parameterized over other types.

Go Generics Type System Generic Function func Process[T any](data T) T Type Parameter: T Constraint: any Type Constraints • any (interface{}) • comparable • Custom interfaces • Union types Concrete Types Process[int](42) Process[string](\"hello\") Process[MyStruct](obj) Type inference available applies satisfies Type Inference Process 1. Analyze function call arguments 2. Match argument types to parameters 3. Instantiate generic with concrete types Compile Time ✓ Type checking ✓ Constraint validation ✓ Code generation ✓ Monomorphization No runtime overhead Runtime ✓ Concrete type execution ✓ Native performance ✗ No type parameters ✗ No reflection needed Same as non-generic code Type Safety + Performance + Code Reuse

Type Parameters

Placeholders for types that are specified when the generic function or type is instantiated. Written in square brackets after the function name.

T any K, V

Type Constraints

Interfaces that specify what operations are allowed on type parameters. They define the contract that concrete types must satisfy.

Ordered comparable

Type Inference

Go's ability to automatically determine type arguments from function call arguments, reducing the need for explicit type specification.

Automatic Smart

Introduction to Generics

Generics allow writing flexible, reusable functions and types that work with any data type.

Basic Generic Functions

package main

import "fmt"

// Generic function with type parameter T
func Min[T int | float64](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// Multiple type parameters
func Swap[T, U any](a T, b U) (U, T) {
    return b, a
}

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

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

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
}

func main() {
    fmt.Println(Min(5, 3))        // 3
    fmt.Println(Min(5.5, 3.2))    // 3.2
    
    x, y := Swap("hello", 42)
    fmt.Printf("x=%v, y=%v\n", x, y) // x=42, y=hello
    
    numbers := []int{1, 2, 3, 4, 5}
    doubled := Map(numbers, func(n int) int { return n * 2 })
    fmt.Println(doubled) // [2 4 6 8 10]
}

Type Constraints

Define constraints to specify what types can be used with generics.

// Basic constraints
type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64 | ~string
}

func Sort[T Ordered](slice []T) {
    // Bubble sort implementation
    n := len(slice)
    for i := 0; i < n-1; i++ {
        for j := 0; j < n-i-1; j++ {
            if slice[j] > slice[j+1] {
                slice[j], slice[j+1] = slice[j+1], slice[j]
            }
        }
    }
}

// Custom constraint interfaces
type Number interface {
    ~int | ~int64 | ~float64
}

type Addable[T any] interface {
    Add(T) T
}

// Type with methods satisfying constraint
type Vector2D struct {
    X, Y float64
}

func (v Vector2D) Add(other Vector2D) Vector2D {
    return Vector2D{X: v.X + other.X, Y: v.Y + other.Y}
}

func Sum[T Addable[T]](items []T) T {
    var result T
    for _, item := range items {
        result = result.Add(item)
    }
    return result
}

// Union and intersection constraints
type SignedInteger interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type UnsignedInteger interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type Integer interface {
    SignedInteger | UnsignedInteger
}

func Abs[T SignedInteger](x T) T {
    if x < 0 {
        return -x
    }
    return x
}

Generic Types

Create generic structs and interfaces for flexible data structures.

// Generic stack
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func (s *Stack[T]) IsEmpty() bool {
    return len(s.items) == 0
}

// Generic linked list
type Node[T any] struct {
    Value T
    Next  *Node[T]
}

type LinkedList[T any] struct {
    Head *Node[T]
    Size int
}

func (l *LinkedList[T]) Add(value T) {
    newNode := &Node[T]{Value: value}
    if l.Head == nil {
        l.Head = newNode
    } else {
        current := l.Head
        for current.Next != nil {
            current = current.Next
        }
        current.Next = newNode
    }
    l.Size++
}

// Generic result type
type Result[T any] struct {
    Value T
    Error error
}

func (r Result[T]) IsOk() bool {
    return r.Error == nil
}

func (r Result[T]) Unwrap() T {
    if r.Error != nil {
        panic(r.Error)
    }
    return r.Value
}

// Generic optional type
type Optional[T any] struct {
    value *T
}

func Some[T any](value T) Optional[T] {
    return Optional[T]{value: &value}
}

func None[T any]() Optional[T] {
    return Optional[T]{value: nil}
}

func (o Optional[T]) IsPresent() bool {
    return o.value != nil
}

func (o Optional[T]) Get() (T, bool) {
    if o.value != nil {
        return *o.value, true
    }
    var zero T
    return zero, false
}

Advanced Generic Patterns

Complex generic patterns for advanced use cases.

// Generic builder pattern
type Builder[T any] struct {
    value T
    err   error
}

func NewBuilder[T any](initial T) *Builder[T] {
    return &Builder[T]{value: initial}
}

func (b *Builder[T]) Transform(fn func(T) (T, error)) *Builder[T] {
    if b.err != nil {
        return b
    }
    b.value, b.err = fn(b.value)
    return b
}

func (b *Builder[T]) Build() (T, error) {
    return b.value, b.err
}

// Generic repository pattern
type Repository[T any, ID comparable] interface {
    FindByID(ID) (T, error)
    Save(T) error
    Delete(ID) error
    FindAll() ([]T, error)
}

type InMemoryRepository[T any, ID comparable] struct {
    data map[ID]T
    mu   sync.RWMutex
}

func NewInMemoryRepository[T any, ID comparable]() *InMemoryRepository[T, ID] {
    return &InMemoryRepository[T, ID]{
        data: make(map[ID]T),
    }
}

func (r *InMemoryRepository[T, ID]) FindByID(id ID) (T, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    if item, ok := r.data[id]; ok {
        return item, nil
    }
    var zero T
    return zero, fmt.Errorf("not found")
}

// Generic pipeline
type Pipeline[T any] struct {
    stages []func(T) T
}

func (p *Pipeline[T]) AddStage(fn func(T) T) *Pipeline[T] {
    p.stages = append(p.stages, fn)
    return p
}

func (p *Pipeline[T]) Process(input T) T {
    result := input
    for _, stage := range p.stages {
        result = stage(result)
    }
    return result
}

// Type inference with generics
func Reduce[T, R any](slice []T, initial R, fn func(R, T) R) R {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

// Usage with type inference
numbers := []int{1, 2, 3, 4, 5}
sum := Reduce(numbers, 0, func(acc, n int) int {
    return acc + n
}) // Type parameters inferred

Performance Considerations

Understanding the performance implications of generics.

// Generics vs interfaces benchmark
type Comparable interface {
    Less(other Comparable) bool
}

// Interface-based approach
func SortInterface(items []Comparable) {
    // sorting logic
}

// Generic approach (more efficient)
func SortGeneric[T Ordered](items []T) {
    // sorting logic - no interface overhead
}

// When to use generics vs interfaces:
// 1. Use generics for:
//    - Type safety at compile time
//    - Better performance (no boxing/unboxing)
//    - Working with basic types

// 2. Use interfaces for:
//    - Runtime polymorphism
//    - When behavior is more important than type
//    - Plugin architectures

// Generic cache with expiration
type Cache[K comparable, V any] struct {
    mu      sync.RWMutex
    items   map[K]cacheItem[V]
    ttl     time.Duration
}

type cacheItem[V any] struct {
    value     V
    expiresAt time.Time
}

func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] {
    cache := &Cache[K, V]{
        items: make(map[K]cacheItem[V]),
        ttl:   ttl,
    }
    go cache.cleanup()
    return cache
}

func (c *Cache[K, V]) Set(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.items[key] = cacheItem[V]{
        value:     value,
        expiresAt: time.Now().Add(c.ttl),
    }
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    item, exists := c.items[key]
    if !exists || time.Now().After(item.expiresAt) {
        var zero V
        return zero, false
    }
    return item.value, true
}

Advanced Generic Patterns

Sophisticated patterns that demonstrate the full power of Go's generics system.

// Functional programming with generics
type Functor[T any] interface {
    Map(func(T) T) Functor[T]
}

type Option[T any] struct {
    value *T
}

func Some[T any](v T) Option[T] {
    return Option[T]{value: &v}
}

func None[T any]() Option[T] {
    return Option[T]{value: nil}
}

func (o Option[T]) Map(f func(T) T) Functor[T] {
    if o.value == nil {
        return None[T]()
    }
    return Some(f(*o.value))
}

func (o Option[T]) FlatMap[U any](f func(T) Option[U]) Option[U] {
    if o.value == nil {
        return None[U]()
    }
    return f(*o.value)
}

// Generic Result type for error handling
type Result[T any] struct {
    value T
    err   error
}

func Ok[T any](value T) Result[T] {
    return Result[T]{value: value, err: nil}
}

func Err[T any](err error) Result[T] {
    var zero T
    return Result[T]{value: zero, err: err}
}

func (r Result[T]) Map[U any](f func(T) U) Result[U] {
    if r.err != nil {
        return Err[U](r.err)
    }
    return Ok(f(r.value))
}

func (r Result[T]) FlatMap[U any](f func(T) Result[U]) Result[U] {
    if r.err != nil {
        return Err[U](r.err)
    }
    return f(r.value)
}

Generics vs Alternatives Comparison

When to Choose Generics Over Alternatives

Understanding the trade-offs between generics, interfaces, and code generation helps you make the right architectural decisions.

Approach Type Safety Performance Code Reuse Complexity Best For
Generics Compile-time Zero overhead High Medium Type-safe collections, algorithms
Interfaces Runtime Virtual dispatch High Low Polymorphism, plugin systems
interface{} Runtime/None Boxing overhead High Low Legacy code, simple utilities
Code Generation Compile-time Optimal High High Build-time customization

Best Practices & Guidelines

Naming Conventions

  • T: Single type parameter
  • K, V: Key-value pairs (maps)
  • E: Element type (collections)
  • R: Return type (transformations)
  • Descriptive: Use meaningful names for complex constraints

Constraint Design

  • Start with minimal constraints
  • Use ~T for underlying type flexibility
  • Compose constraints with embedded interfaces
  • Prefer behavioral constraints over structural ones
  • Document constraint requirements clearly

API Design

  • Favor type inference over explicit parameters
  • Keep generic APIs simple and focused
  • Provide non-generic convenience functions
  • Consider backward compatibility
  • Use generics sparingly in public APIs

When to Use Generics

  • Collections and containers: Slices, maps, sets, queues, stacks
  • Algorithms: Sorting, searching, filtering, transformations
  • Functional patterns: Map, filter, reduce operations
  • Type-safe wrappers: Optional, Result, Either types
  • Mathematical operations: Generic math functions
  • Protocol implementations: Serialization, validation

Common Mistakes

  • Over-generalization: Making everything generic when interfaces suffice
  • Complex constraints: Creating overly complicated type hierarchies
  • Ignoring inference: Always specifying type parameters explicitly
  • Poor naming: Using unclear or inconsistent type parameter names
  • Premature optimization: Adding generics before understanding the use cases

Practice Challenges

Hands-On Projects

Build these generic utilities to master Go's type system:

1. Generic Collections

Implement a complete set of type-safe collections: Set[T], Queue[T], Stack[T], and PriorityQueue[T] with full API.

Beginner comparable

2. Functional Pipeline

Create a functional programming library with chainable operations: Map, Filter, Reduce, GroupBy, and Zip.

Intermediate any

3. Generic ORM

Build a simple ORM with generic query builders, type-safe where clauses, and automatic struct mapping.

Advanced Custom

4. Validation Framework

Design a composable validation system with generic rules, error accumulation, and custom constraint types.

Expert Complex