>_
GolangStepByStep
Software Engineer

Generics

Constraints, type parameters, practical use cases in Go 1.18+

# The Copy-Paste Problem

Imagine you run a factory that builds boxes. You need a box for Apples, a box for Books, and a box for Cables.

Before Generics (pre-Go 1.18), if you wanted a function to remove an item from an array of Apples, you had to write `RemoveApple(arr []Apple)`. If you wanted to do the same for Books, you had to literally copy-paste the exact same code to make `RemoveBook(arr []Book)`. Your codebase quickly filled up with duplicated code doing the exact same logic just for different Variable Types.

Generics fix this. They allow you to write a single, standardized "Blueprint" (a Generic Box function), and the Go compiler automatically prints out the right box for whatever items you give it.

# Level 1: Type Parameters "[T any]" (Beginner)

Let's write a function that prints an array. To make it generic, we simply add square brackets [T any] before the standard parenthesis!

T is simply a placeholder variable holding a Type instead of a value. any tells the compiler "I will accept literally any type you want."

// [T any] says: "Let T represent whichever type the user provides"
func PrintArray[T any](items []T) {
    for _, item := range items {
        fmt.Println(item)
    }
}

func main() {
    // The compiler sees we passed integers, so T automatically becomes 'int'.
    PrintArray([]int{1, 2, 3})
    
    // The compiler sees strings, so T automatically becomes 'string'.
    PrintArray([]string{"A", "B", "C"})
}

Type Inference: Even though we could explicitly call it like PrintArray[int]([]int{1, 2}), the Go compiler is smart enough to just infer the type from your arguments. You almost never have to type the brackets when calling a function!

# Level 2: Type Constraints (Intermediate)

The `any` keyword is great for printing things, but what if you want to perform math?

If you write func Sum[T any](a, b T) T { return a + b }, the compiler throws a massive error! Why? Because what if a developer passes a Map into T? You cannot add two Maps together using +!

We must restrict T by using a Type Constraint. We define an interface listing exactly which types are legally permitted.

// 1. Define a strict list of allowed types using the '|' (Union) symbol
type Number interface {
    int | int64 | float64
}

// 2. We replace 'any' with our strict 'Number' Interface.
func Sum[T Number](a T, b T) T {
    // The compiler allows '+' because it KNOWS all 
    // these types safely support addition!
    return a + b 
}

func main() {
    fmt.Println(Sum(5, 10))        // T becomes int. Valid!
    fmt.Println(Sum(5.5, 2.1))     // T becomes float64. Valid!
    
    // Sum("Hello", "World")       // FATAL ERROR! string does not implement Number.
}

# Level 3: Generic Structs (Advanced)

Functions are easy, but where Generics truly shine is in creating reusable Data Structures. When writing custom Caches, Linked Lists, or Trees, you can now make them totally generic!

Here is how you write a Generic Stack (First-In, Last-Out queue).

// 1. Define a Generic Struct
type Stack[T any] struct {
    items []T
}

// 2. Use the Generic [T] on the method receiver
func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() T {
    if len(s.items) == 0 {
        var zero T // Retrieves the default '0' or 'nil' value for whatever T is!
        return zero
    }
    
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index] // Remove from slice
    return item
}

func main() {
    // We MUST specify the type when building Generic Structs
    intStack := Stack[int]{}
    intStack.Push(10)
    intStack.Push(20)
    fmt.Println(intStack.Pop()) // Prints 20
}

# Level 4: The 'comparable' Constraint & Maps (Expert)

Let's say you want to write a generic function that easily grabs all the Keys out of a Map. What constraints do you need?

A Map has two types: a Key and a Value. In Go, Map values can be anything (`any`). But Map Keys are strictly required to be comparable! A map key must support the `==` operator so the map can find it in memory (you cannot use slices or maps as keys directly).

Go provides a built-in constraint literally called comparable just for this exact scenario.

// K MUST only be a type that supports '=='. V can be 'any'.
func GetKeys[K comparable, V any](m map[K]V) []K {
    var keys []K
    
    for k := range m {
        keys = append(keys, k)
    }
    
    return keys
}

func main() {
    userAges := map[string]int{
        "Alice": 25,
        "Bob":   30,
    }
    
    // K infers string. V infers int. Returns []string{"Alice", "Bob"}
    keys := GetKeys(userAges) 
}

Understanding how to blend [K comparable, V any] with maps entirely unlocks your capability to write powerful generic data transformations like Filter, Map, and Reduce in Go!

practice & review