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.
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, VType Constraints
Interfaces that specify what operations are allowed on type parameters. They define the contract that concrete types must satisfy.
Ordered comparableType Inference
Go's ability to automatically determine type arguments from function call arguments, reducing the need for explicit type specification.
Automatic SmartIntroduction 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 comparable2. Functional Pipeline
Create a functional programming library with chainable operations: Map, Filter, Reduce, GroupBy, and Zip.
Intermediate any3. Generic ORM
Build a simple ORM with generic query builders, type-safe where clauses, and automatic struct mapping.
Advanced Custom4. Validation Framework
Design a composable validation system with generic rules, error accumulation, and custom constraint types.
Expert Complex