Web Analytics

Generics & Reflection

Advanced ~50 min read

Go 1.18 introduced generics, enabling type-safe code reuse without sacrificing performance. Combined with reflection for runtime type inspection, these features provide powerful tools for building flexible, maintainable applications. Master both to write modern, idiomatic Go code.

Generics in Go - Type Parameters and Constraints

Figure: Go Generics - Type Parameters, Generic Functions, Types, and Constraints

Introduction to Generics

Generics allow you to write functions and types that work with any type while maintaining type safety. The syntax uses square brackets [T any] for type parameters:

Output
Click Run to execute your code
Type Parameter Syntax:
  • [T any] - T is the type parameter, any is the constraint
  • any - Built-in constraint meaning "any type" (alias for interface{})
  • Type inference - Go can often infer T from arguments

Generic Functions

Generic functions can work with multiple types and transform data type-safely:

Output
Click Run to execute your code
When to Use Generics:
  • Data structures (stacks, queues, trees)
  • Algorithms (sorting, searching, mapping)
  • Utility functions (Min, Max, Filter, Map)
  • When you'd otherwise use interface{} with type assertions

Generic Types

Create reusable data structures that work with any type:

Output
Click Run to execute your code
Pro Tip: Generic types are instantiated with specific types like Stack[int] or Stack[string]. Each instantiation creates a separate type.

Type Constraints

Constraints limit which types can be used with generics:

Output
Click Run to execute your code
Constraint Package Description
any Built-in Any type (alias for interface{})
comparable Built-in Types that support == and !=
constraints.Ordered golang.org/x/exp/constraints Types that support <, >, <=, >=
Custom Your code Interface with type union

Reflection Basics

The reflect package provides runtime type inspection:

Output
Click Run to execute your code
Reflection Concepts:
  • reflect.TypeOf() - Get type information
  • reflect.ValueOf() - Get value information
  • Type.Kind() - Get underlying kind (struct, int, etc.)
  • Type.Field() - Access struct fields
  • Field.Tag - Read struct tags

Advanced Reflection

Reflection enables dynamic method calls and field modification:

Output
Click Run to execute your code
Reflection Performance: Reflection is slower than direct code. Use it when flexibility is more important than performance (serialization, ORMs, dependency injection).

Generics vs Reflection

Aspect Generics Reflection
Type Safety โœ… Compile-time โŒ Runtime only
Performance โœ… Fast (no overhead) โš ๏ธ Slower
Flexibility โš ๏ธ Limited to constraints โœ… Very flexible
Use Case Data structures, algorithms Serialization, ORMs, DI

Common Mistakes

1. Over-using generics

// โŒ Wrong - generics not needed
func AddInts[T int](a, b T) T {
    return a + b
}

// โœ… Correct - just use int
func AddInts(a, b int) int {
    return a + b
}

2. Forgetting to check CanSet() in reflection

// โŒ Wrong - will panic
v := reflect.ValueOf(myStruct)
v.FieldByName("Name").SetString("Alice")

// โœ… Correct - use pointer and check
v := reflect.ValueOf(&myStruct).Elem()
field := v.FieldByName("Name")
if field.CanSet() {
    field.SetString("Alice")
}

Exercise: Generic Cache with Reflection

Task: Build a generic cache that uses reflection to validate stored values.

Requirements:

  • Generic Cache[K comparable, V any] type
  • Get/Set methods
  • Use reflection to log type information
  • Validate that keys are comparable
Show Solution
package main

import (
    "fmt"
    "reflect"
    "sync"
)

type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    c := &Cache[K, V]{
        items: make(map[K]V),
    }
    
    // Log type information using reflection
    var k K
    var v V
    fmt.Printf("Created cache with key type: %v, value type: %v\n",
        reflect.TypeOf(k), reflect.TypeOf(v))
    
    return c
}

func (c *Cache[K, V]) Set(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    // Use reflection to log value details
    vType := reflect.TypeOf(value)
    vValue := reflect.ValueOf(value)
    fmt.Printf("Setting key=%v, value type=%v, kind=%v\n",
        key, vType, vValue.Kind())
    
    c.items[key] = value
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    value, ok := c.items[key]
    return value, ok
}

func main() {
    // String -> int cache
    cache1 := NewCache[string, int]()
    cache1.Set("age", 25)
    cache1.Set("score", 100)
    
    if age, ok := cache1.Get("age"); ok {
        fmt.Printf("Age: %d\n", age)
    }
    
    // Int -> struct cache
    type Person struct {
        Name string
        Age  int
    }
    
    cache2 := NewCache[int, Person]()
    cache2.Set(1, Person{"Alice", 25})
    cache2.Set(2, Person{"Bob", 30})
    
    if person, ok := cache2.Get(1); ok {
        fmt.Printf("Person: %+v\n", person)
    }
}

Summary

  • Generics (Go 1.18+) enable type-safe code reuse
  • Type parameters use syntax [T any]
  • Constraints limit which types can be used
  • Type inference often eliminates need to specify types
  • Generic types create reusable data structures
  • Reflection provides runtime type inspection
  • reflect.TypeOf() gets type information
  • reflect.ValueOf() gets value information
  • Prefer generics over reflection for performance
  • Use reflection when you need runtime flexibility

Congratulations!

You've completed the advanced Go topics! You now have a comprehensive understanding of Go's modern features including generics and reflection. These tools, combined with everything you've learned, make you ready to build production-grade Go applications.