Go Gopher How to Go

Generics (type parameters) were introduced in Go 1.18, allowing you to write functions and types that work with multiple types while maintaining type safety. Generics eliminate code duplication and enable powerful abstractions.

💡 Key Points

  • Generics enable type-safe reusable code
  • Use square brackets [] for type parameters
  • Type constraints specify what operations are allowed
  • any is a constraint that accepts any type
  • comparable constraint for types that support == and !=
  • Can define custom constraints with interfaces
  • Generics work with functions, types, and methods
  • Type inference often eliminates need to specify types explicitly

Generic Functions

Define functions that work with multiple types:

package main

import "fmt"

// Generic function with type parameter T
func Print[T any](value T) {
    fmt.Println(value)
}

// Generic function with constraint
func Min[T comparable](a, b T) T {
    // Note: This won't compile as-is because comparable
    // doesn't include < operator. See ordered constraint below.
    if a < b {
        return a
    }
    return b
}

// Using constraints package for ordered types
import "golang.org/x/exp/constraints"

func MinOrdered[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    // Type inference - Go figures out the type
    Print(42)
    Print("hello")
    Print(3.14)
    Print([]int{1, 2, 3})
    
    // Explicit type parameter
    Print[string]("world")
    
    // Using ordered constraint
    fmt.Println("Min int:", MinOrdered(10, 20))
    fmt.Println("Min float:", MinOrdered(3.14, 2.71))
    fmt.Println("Min string:", MinOrdered("apple", "banana"))
}
Output:
42
hello
3.14
[1 2 3]
world
Min int: 10
Min float: 2.71
Min string: apple

Generic Types

Create data structures that work with any type:

package main

import "fmt"

// Generic stack type
type Stack[T any] struct {
    items []T
}

// Methods on generic type
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 function
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 main() {
    // Stack of integers
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)
    
    val, ok := intStack.Pop()
    fmt.Printf("Popped: %d (ok: %v)\n", val, ok)
    
    // Stack of strings
    strStack := Stack[string]{}
    strStack.Push("hello")
    strStack.Push("world")
    
    str, ok := strStack.Pop()
    fmt.Printf("Popped: %s (ok: %v)\n", str, ok)
    
    // Generic map function
    numbers := []int{1, 2, 3, 4, 5}
    doubled := Map(numbers, func(n int) int { return n * 2 })
    fmt.Println("Doubled:", doubled)
    
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("num_%d", n)
    })
    fmt.Println("Strings:", strings)
}
Output:
Popped: 3 (ok: true)
Popped: world (ok: true)
Doubled: [2 4 6 8 10]
Strings: [num_1 num_2 num_3 num_4 num_5]

Type Constraints

ConstraintDescriptionExample
anyAccepts any type (alias for interface{})func Print[T any](v T)
comparableTypes that support == and !=func Equal[T comparable](a, b T)
constraints.OrderedTypes that support <, >, etc.func Max[T constraints.Ordered](a, b T)
constraints.IntegerAll integer typesfunc Sum[T constraints.Integer](nums []T)
constraints.FloatAll float typesfunc Avg[T constraints.Float](nums []T)
Custom interfaceDefine your own constrainttype Numeric interface { int | float64 }

Custom Constraints

PatternExampleUse Case
Type unioninterface { int | float64 | string }Allow specific types
Method constraintinterface { String() string }Require specific methods
Combined constraintinterface { ~int | ~float64; String() string }Type union + methods
Approximate typeinterface { ~int }Underlying type must be int
Embedded constraintinterface { Stringer; comparable }Combine multiple constraints

Generic Syntax

ElementSyntaxDescription
Type parameter[T any]Single type parameter
Multiple params[T, U any]Multiple type parameters
Different constraints[T any, U comparable]Each param with own constraint
Generic functionfunc Name[T any](v T) { }Function with type parameter
Generic typetype Box[T any] struct { }Type with type parameter
Method on genericfunc (b Box[T]) Get() T { }Method on generic type

Common Generic Patterns

PatternExampleUse Case
Generic containertype List[T any] struct { }Lists, stacks, queues
Generic algorithmfunc Sort[T Ordered](slice []T)Sorting, searching
Map/Filter/Reducefunc Map[T, U any](s []T, f func(T)U) []UFunctional operations
Generic pairtype Pair[T, U any] struct { First T; Second U }Tuple-like structures
Generic optiontype Option[T any] struct { valid bool; value T }Optional values
Generic resulttype Result[T any] struct { value T; err error }Result with error

Generics Best Practices

GuidelineReasonExample
Use when type-safe reuse neededAvoid code duplicationGeneric containers, algorithms
Keep constraints simpleEasier to understand and usePrefer any, comparable
Consider interfaces firstMay be simpler for behaviorInterface methods vs type params
Don't over-generalizeYAGNI principleAdd generics when actually needed
Use type inferenceLess verbose codeLet Go infer types when possible
Name conventionsClarity and consistencyT for type, K/V for key/value