Generics & Reflection
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.
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:
Click Run to execute your code
[T any]- T is the type parameter, any is the constraintany- 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:
Click Run to execute your code
- 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:
Click Run to execute your code
Stack[int] or Stack[string]. Each instantiation
creates a
separate type.
Type Constraints
Constraints limit which types can be used with generics:
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:
Click Run to execute your code
reflect.TypeOf()- Get type informationreflect.ValueOf()- Get value informationType.Kind()- Get underlying kind (struct, int, etc.)Type.Field()- Access struct fieldsField.Tag- Read struct tags
Advanced Reflection
Reflection enables dynamic method calls and field modification:
Click Run to execute your code
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.
Enjoying these tutorials?