Go Generics

Type parameters and generic programming in Go

🔧 What are Go Generics?

Go Generics allow you to write flexible, reusable code that works with different types. Introduced in Go 1.18, generics enable type-safe functions and data structures without code duplication.


// Generic function example
package main

import "fmt"

func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Max(10, 20))        // int
    fmt.Println(Max(3.14, 2.71))    // float64
    fmt.Println(Max("hello", "world")) // string
}
                                    

Output:

20
3.14
world

Generic Programming Features

🔤

Type Parameters

Define flexible function parameters

func Name[T any](param T) T
📋

Type Constraints

Limit which types can be used

func Name[T comparable](param T)
📦

Generic Types

Create reusable data structures

type Stack[T any] struct{}
🔗

Type Inference

Automatic type detection

result := Max(10, 20) // inferred

🔹 Generic Functions

Create functions that work with multiple types:

package main

import "fmt"

// Generic function with any constraint
func Print[T any](value T) {
    fmt.Printf("Value: %v, Type: %T\n", value, value)
}

// Generic function with comparable constraint
func Equal[T comparable](a, b T) bool {
    return a == b
}

// Generic function with custom constraint
func Sum[T int | float64](slice []T) T {
    var sum T
    for _, v := range slice {
        sum += v
    }
    return sum
}

func main() {
    // Using generic functions
    Print(42)
    Print("Hello")
    Print(3.14)
    
    fmt.Println("Equal:", Equal(5, 5))
    fmt.Println("Equal:", Equal("go", "go"))
    
    ints := []int{1, 2, 3, 4, 5}
    floats := []float64{1.1, 2.2, 3.3}
    
    fmt.Println("Sum of ints:", Sum(ints))
    fmt.Println("Sum of floats:", Sum(floats))
}

Output:

Value: 42, Type: int
Value: Hello, Type: string
Value: 3.14, Type: float64
Equal: true
Equal: true
Sum of ints: 15
Sum of floats: 6.6

🔹 Generic Data Structures

Build reusable data structures with generics:

package main

import "fmt"

// 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) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

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

// Generic Map with custom key-value types
type SafeMap[K comparable, V any] struct {
    data map[K]V
}

func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
    return &SafeMap[K, V]{
        data: make(map[K]V),
    }
}

func (m *SafeMap[K, V]) Set(key K, value V) {
    m.data[key] = value
}

func (m *SafeMap[K, V]) Get(key K) (V, bool) {
    value, exists := m.data[key]
    return value, exists
}

func main() {
    // String stack
    stringStack := &Stack[string]{}
    stringStack.Push("first")
    stringStack.Push("second")
    
    item, ok := stringStack.Pop()
    fmt.Printf("Popped: %s, Success: %t\n", item, ok)
    
    // Integer stack
    intStack := &Stack[int]{}
    intStack.Push(10)
    intStack.Push(20)
    
    item2, ok2 := intStack.Pop()
    fmt.Printf("Popped: %d, Success: %t\n", item2, ok2)
    
    // Generic map
    userMap := NewSafeMap[string, int]()
    userMap.Set("alice", 25)
    userMap.Set("bob", 30)
    
    age, exists := userMap.Get("alice")
    fmt.Printf("Alice's age: %d, Exists: %t\n", age, exists)
}

Output:

Popped: second, Success: true
Popped: 20, Success: true
Alice's age: 25, Exists: true

🔹 Type Constraints

Define custom constraints for more specific generic functions:

package main

import "fmt"

// Custom constraint using interface
type Numeric interface {
    int | int32 | int64 | float32 | float64
}

// Custom constraint with methods
type Stringer interface {
    String() string
}

// Generic function with numeric constraint
func Add[T Numeric](a, b T) T {
    return a + b
}

// Generic function with method constraint
func PrintString[T Stringer](item T) {
    fmt.Println("String representation:", item.String())
}

// Combining constraints
type Ordered interface {
    int | int32 | int64 | float32 | float64 | string
}

func Min[T Ordered](slice []T) T {
    if len(slice) == 0 {
        var zero T
        return zero
    }
    
    min := slice[0]
    for _, v := range slice[1:] {
        if v < min {
            min = v
        }
    }
    return min
}

// Custom type implementing Stringer
type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}

func main() {
    // Using numeric constraint
    fmt.Println("Add integers:", Add(10, 20))
    fmt.Println("Add floats:", Add(3.14, 2.86))
    
    // Using method constraint
    person := Person{Name: "Alice", Age: 30}
    PrintString(person)
    
    // Using ordered constraint
    numbers := []int{5, 2, 8, 1, 9}
    words := []string{"zebra", "apple", "banana"}
    
    fmt.Println("Min number:", Min(numbers))
    fmt.Println("Min word:", Min(words))
}

Output:

Add integers: 30
Add floats: 6
String representation: Alice (30 years old)
Min number: 1
Min word: apple

🔹 Generic Algorithms

Implement common algorithms using generics:

package main

import "fmt"

// Generic filter function
func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, item := range slice {
        if predicate(item) {
            result = append(result, item)
        }
    }
    return result
}

// Generic map function
func Map[T, U any](slice []T, transform func(T) U) []U {
    result := make([]U, len(slice))
    for i, item := range slice {
        result[i] = transform(item)
    }
    return result
}

// Generic reduce function
func Reduce[T, U any](slice []T, initial U, reducer func(U, T) U) U {
    result := initial
    for _, item := range slice {
        result = reducer(result, item)
    }
    return result
}

// Generic find function
func Find[T any](slice []T, predicate func(T) bool) (T, bool) {
    for _, item := range slice {
        if predicate(item) {
            return item, true
        }
    }
    var zero T
    return zero, false
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    // Filter even numbers
    evens := Filter(numbers, func(n int) bool {
        return n%2 == 0
    })
    fmt.Println("Even numbers:", evens)
    
    // Map numbers to strings
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("num_%d", n)
    })
    fmt.Println("Mapped strings:", strings[:3], "...")
    
    // Reduce to sum
    sum := Reduce(numbers, 0, func(acc, n int) int {
        return acc + n
    })
    fmt.Println("Sum:", sum)
    
    // Find first number > 5
    found, exists := Find(numbers, func(n int) bool {
        return n > 5
    })
    fmt.Printf("First number > 5: %d, Found: %t\n", found, exists)
}

Output:

Even numbers: [2 4 6 8 10]
Mapped strings: [num_1 num_2 num_3] ...
Sum: 55
First number > 5: 6, Found: true

🧠 Test Your Knowledge

What constraint allows any type in Go generics?