Lab 14: Generics

Objective

Write type-safe generic functions and data structures using Go's generics: type parameters, constraints, comparable, any, and the constraints package.

Time

30 minutes

Prerequisites

  • Lab 03 (Slices & Maps), Lab 05 (Interfaces)

Tools

  • Docker image: zchencow/innozverse-go:latest (Go 1.22)


Lab Instructions

Step 1: Generic Functions

docker run --rm zchencow/innozverse-go:latest go run - << 'EOF'
package main

import "fmt"

// Generic Min/Max — T must be ordered
type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64 | ~string
}

func Min[T Ordered](a, b T) T {
    if a < b { return a }
    return b
}

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

func Clamp[T Ordered](val, lo, hi T) T {
    return Max(lo, Min(val, hi))
}

// Generic slice functions
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice { result[i] = fn(v) }
    return result
}

func Filter[T any](slice []T, pred func(T) bool) []T {
    result := make([]T, 0)
    for _, v := range slice {
        if pred(v) { result = append(result, v) }
    }
    return result
}

func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    acc := initial
    for _, v := range slice { acc = fn(acc, v) }
    return acc
}

func Contains[T comparable](slice []T, val T) bool {
    for _, v := range slice {
        if v == val { return true }
    }
    return false
}

func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m { keys = append(keys, k) }
    return keys
}

func Values[K comparable, V any](m map[K]V) []V {
    vals := make([]V, 0, len(m))
    for _, v := range m { vals = append(vals, v) }
    return vals
}

func main() {
    fmt.Println("Min(3,5):", Min(3, 5))
    fmt.Println("Min(a,b):", Min("apple", "banana"))
    fmt.Println("Max(3.14,2.71):", Max(3.14, 2.71))
    fmt.Println("Clamp(15,0,10):", Clamp(15, 0, 10))
    fmt.Println("Clamp(-5,0,10):", Clamp(-5, 0, 10))
    fmt.Println("Clamp(7,0,10):", Clamp(7, 0, 10))

    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    doubled := Map(nums, func(n int) int { return n * 2 })
    evens   := Filter(nums, func(n int) bool { return n%2 == 0 })
    sum     := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    strs    := Map(nums, func(n int) string { return fmt.Sprintf("#%d", n) })

    fmt.Println("Doubled:", doubled)
    fmt.Println("Evens:", evens)
    fmt.Println("Sum:", sum)
    fmt.Println("Strings:", strs[:3], "...")
    fmt.Println("Contains 7:", Contains(nums, 7))
    fmt.Println("Contains 11:", Contains(nums, 11))
}
EOF

💡 ~int in a constraint means "any type whose underlying type is int" — this includes custom types like type UserID int. Without ~, type UserID int would NOT satisfy int. The tilde prefix enables user-defined types to satisfy constraints based on their underlying types.

📸 Verified Output:


Step 2: Generic Data Structures

💡 [T any] in struct definition creates a generic type — Stack[string] and Stack[int] are separate types with their own methods. Go generics are monomorphized at compile time (like C++ templates), so there's no runtime overhead from type erasure (unlike Java generics).

📸 Verified Output:


Steps 3–8: Type constraints, Pipeline, Cache, Optional, Sets, Capstone

📸 Verified Output:


Summary

Feature
Syntax
Use case

Type parameter

func F[T any]

Generic function

Constraint

interface { ~int | ~string }

Restrict allowed types

comparable

Built-in constraint

Types that support ==

Multiple params

func F[T, U any]

Input/output type differ

Generic struct

type Stack[T any] struct

Reusable data structures

Tilde ~

~int

Includes named types over int

Further Reading

Last updated