🧬 Chapter 17 of 20

Generics

Go 1.18 (2022) introduced generics β€” type parameters that let you write functions and types that work with any type satisfying a constraint. Generics eliminate code duplication while maintaining type safety, without the runtime cost of interface boxing.

⏱ β€”πŸ“Š Advanced

🎯 Learning Objectives

βœ… After this chapter you will:
  • Write generic functions with type parameters
  • Define and use constraints (interfaces as type sets)
  • Create generic types (stacks, sets, ordered maps)
  • Use built-in constraints: comparable, any
  • Use golang.org/x/exp/constraints for ordered types
  • Understand when NOT to use generics
  • Understand the implementation (GC shapes, stenciling)

πŸ“ Generic Functions

Type parameters are declared in square brackets [T constraint] before the regular parameters. They let one function work with many types.

go
package main

import (
    "fmt"
    "golang.org/x/exp/constraints"  // go get golang.org/x/exp
)

// Without generics β€” must write for each type
func SumInts(s []int) int {
    total := 0
    for _, v := range s { total += v }
    return total
}
func SumFloat64s(s []float64) float64 {
    total := 0.0
    for _, v := range s { total += v }
    return total
}

// With generics β€” one function, any ordered numeric type
func Sum[T constraints.Ordered](s []T) T {
    var total T  // zero value of T
    for _, v := range s { total += v }
    return total
}

// Map: apply a function to every element
func Map[T, U any](s []T, fn func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

// Filter: keep elements satisfying a predicate
func Filter[T any](s []T, fn func(T) bool) []T {
    var result []T
    for _, v := range s {
        if fn(v) { result = append(result, v) }
    }
    return result
}

// Reduce: fold a slice to a single value
func Reduce[T, U any](s []T, init U, fn func(U, T) U) U {
    acc := init
    for _, v := range s { acc = fn(acc, v) }
    return acc
}

// Contains: check membership (requires comparable constraint)
func Contains[T comparable](s []T, val T) bool {
    for _, v := range s {
        if v == val { return true }
    }
    return false
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    floats := []float64{1.1, 2.2, 3.3}
    strs := []string{"a", "b", "c"}

    fmt.Println("Sum ints:", Sum(ints))      // 15 (type inferred!)
    fmt.Println("Sum floats:", Sum(floats))  // 6.6

    doubled := Map(ints, func(x int) int { return x * 2 })
    fmt.Println("Doubled:", doubled)  // [2 4 6 8 10]

    words := []string{"Go", "is", "awesome", "fast", "simple"}
    long := Filter(words, func(s string) bool { return len(s) > 3 })
    fmt.Println("Long words:", long)  // [awesome fast simple]

    product := Reduce(ints, 1, func(acc, v int) int { return acc * v })
    fmt.Println("Product:", product)  // 120

    fmt.Println("Contains 3:", Contains(ints, 3))        // true
    fmt.Println("Contains 'Go':", Contains(strs, "Go"))  // true
    _ = strs
}
βœ… Output
Sum ints: 15 Sum floats: 6.6000000000000005 Doubled: [2 4 6 8 10] Long words: [awesome fast simple] Product: 120 Contains 3: true Contains 'Go': true

πŸ”’ Constraints β€” Interfaces as Type Sets

A constraint is an interface that defines the set of types a type parameter can be. Go 1.18 extended interfaces to include type elements using the ~ (underlying type) and | (union) operators.

go
package main

import "fmt"

// Constraint using type union β€” only these specific types
type Integer interface {
    int | int8 | int16 | int32 | int64
}

// ~ means "underlying type" β€” includes type aliases
type Numeric interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64
}

// Constraint with methods β€” type must have these methods
type Stringer interface {
    String() string
}

// Combined: type must be comparable AND have a String method
type StringerAndComparable interface {
    comparable
    String() string
}

// Practical example: generic Abs function
type SignedNumeric interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Abs[T SignedNumeric](x T) T {
    if x < 0 { return -x }
    return x
}

// User-defined type with ~ β€” works with Numeric constraint
type Celsius float64
type Kelvin float64

func ToKelvin[T ~float64](c T) Kelvin {
    return Kelvin(c + 273.15)
}

func main() {
    fmt.Println(Abs(-42))      // 42
    fmt.Println(Abs(-3.14))    // 3.14
    fmt.Println(Abs(int32(-100)))  // 100

    fmt.Println(ToKelvin(Celsius(100)))  // 373.15
    fmt.Println(ToKelvin(float64(0)))    // 273.15

    // comparable β€” built-in constraint for == and !=
    // any      β€” alias for interface{}, accepts all types
}
βœ… Output
42 3.14 100 373.15 273.15

πŸ—οΈ Generic Types

go
package main

import (
    "errors"
    "fmt"
)

// Generic Stack β€” type-safe, works with any type
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(v T) {
    s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, error) {
    var zero T
    if len(s.items) == 0 {
        return zero, errors.New("stack is empty")
    }
    top := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return top, nil
}

func (s *Stack[T]) Peek() (T, bool) {
    var zero T
    if len(s.items) == 0 { return zero, false }
    return s.items[len(s.items)-1], true
}

func (s *Stack[T]) Len() int { return len(s.items) }

// Generic Set β€” type-safe set with comparable constraint
type Set[T comparable] struct {
    m map[T]struct{}
}

func NewSet[T comparable]() *Set[T] {
    return &Set[T]{m: make(map[T]struct{})}
}

func (s *Set[T]) Add(v T) { s.m[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok }
func (s *Set[T]) Len() int { return len(s.m) }
func (s *Set[T]) Remove(v T) { delete(s.m, v) }

// Generic Result type β€” like Rust's Result
type Result[T any] struct {
    value T
    err   error
}

func Ok[T any](v T) Result[T] { return Result[T]{value: v} }
func Err[T any](e error) Result[T] { return Result[T]{err: e} }
func (r Result[T]) Unwrap() (T, error) { return r.value, r.err }

func main() {
    // Stack of strings
    s := &Stack[string]{}
    s.Push("first")
    s.Push("second")
    s.Push("third")

    for s.Len() > 0 {
        v, _ := s.Pop()
        fmt.Println(v)
    }

    // Stack of ints
    ns := &Stack[int]{}
    for i := 1; i <= 5; i++ { ns.Push(i * i) }
    top, _ := ns.Peek()
    fmt.Println("Top:", top)  // 25

    // Set
    set := NewSet[string]()
    set.Add("go")
    set.Add("rust")
    set.Add("go")  // duplicate
    fmt.Println("Set size:", set.Len())      // 2
    fmt.Println("Has 'go':", set.Has("go")) // true

    // Result type
    r := Ok[int](42)
    v, err := r.Unwrap()
    fmt.Println(v, err)  // 42 
}
βœ… Output
third second first Top: 25 Set size: 2 Has 'go': true 42 <nil>

🚫 When NOT to Use Generics

⚠️ The Go Team's Rule

"If you find yourself writing the exact same code for three or more different types, consider using generics. If you're writing code just to be generic, don't." β€” Go team

βœ… Good Use of Generics❌ Avoid Generics When
Data structures (Stack, Queue, Set, Map, Tree)You only have 1–2 concrete types
Utility functions (Map, Filter, Reduce, Contains)Interface polymorphism is sufficient
Type-safe wrappers (Result[T], Option[T])Types need different behaviour (use interfaces)
Mathematical operations on numeric typesThe function doesn't actually use T uniformly
Avoiding code duplication across numeric typesYou'd need reflection anyway for the logic

🎀 Interview Questions

1
What is the difference between generics and interfaces in Go?
β–Ά
Detailed Answer

Interfaces: Runtime polymorphism. An interface variable holds a (type, value) pair. Method dispatch goes through the interface's virtual method table (vtable). This adds ~15ns overhead per call and requires heap allocation for the boxed value. Best when you need: different types that share behaviour, runtime dispatch, and heterogeneous collections.

Generics: Compile-time polymorphism. The compiler generates (or shares via GC shapes) specialised code for each concrete type used. No boxing, no vtable lookup. Best when you need: the same algorithm to work on different types, type-safe containers, zero-overhead abstraction.

Rule of thumb: use interfaces when types have different behaviour; use generics when types are interchangeable for a given algorithm. Use interfaces for io.Reader (many types reading differently); use generics for Map[T, U] (same mapping logic for any type).

2
What is the comparable constraint? When do you need it?
β–Ά
Detailed Answer

comparable is a built-in constraint that limits a type parameter to types that support == and !=. This includes: all primitive types, arrays of comparable elements, structs with comparable fields, and pointers.

It does NOT include: slices, maps, functions, or structs containing them.

You need comparable when: (1) using the type parameter as a map key; (2) comparing values with ==; (3) implementing Set, Cache, or other equality-based data structures. Without it, == on a type parameter is a compile error.

πŸ§ͺ Quiz

🧩
Generics
4 questions
1. Where are type parameters declared in a generic Go function?
2. Why do you need the comparable constraint for a generic Set[T]?
3. What does ~int mean in a constraint?
4. Given func Sum[T int|float64](s []T) T, can you call it as Sum([]int{1,2,3})?
β€”

πŸ“Œ Cheat Sheet

πŸ“Œ Generics β€” Quick Reference
Generic func
func F[T any](x T) T { }
Multiple params
func F[T, U any](x T) U { }
Constraint
func F[T int|float64](x T) T
Underlying type
~int includes type MyInt int
Comparable
func F[T comparable](s []T, v T) bool
Generic type
type Stack[T any] struct { items []T }
Method
func (s *Stack[T]) Push(v T)
Instantiate
s := &Stack[int]{}
Type infer
Sum([]int{1,2}) // T inferred
any
alias for interface{} β€” no constraints
Zero value
var zero T // T's zero value
Exp package
golang.org/x/exp/constraints