Generics in Go

Generics, introduced in Go 1.18, allow you to write functions and types that work with multiple types while maintaining type safety. This feature helps reduce code duplication and makes your code more flexible.

Generic Functions

A generic function can work with different types. Use square brackets to define type parameters:

package main

import "fmt"

// Generic function that works with any comparable type
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item {
            return true
        }
    }
    return false
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    fmt.Println(Contains(ints, 3)) // true
    
    strings := []string{"apple", "banana", "cherry"}
    fmt.Println(Contains(strings, "banana")) // true
}

The T is a type parameter that gets replaced with the actual type when you call the function.

Type Constraints

You can constrain type parameters to certain behaviors using interfaces:

package main

import "fmt"

// Number interface for numeric types
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

// Generic sum function for numbers
func Sum[T Number](numbers []T) T {
    var total T
    for _, num := range numbers {
        total += num
    }
    return total
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    fmt.Println(Sum(ints)) // 15
    
    floats := []float64{1.1, 2.2, 3.3}
    fmt.Println(Sum(floats)) // 6.6
}

Generic Types

You can define generic types as well:

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 {
    if len(s.items) == 0 {
        panic("stack is empty")
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item
}

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

func main() {
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    fmt.Println(intStack.Pop()) // 2
    
    stringStack := Stack[string]{}
    stringStack.Push("hello")
    stringStack.Push("world")
    fmt.Println(stringStack.Pop()) // "world"
}

Generic Methods

Methods can also be generic:

package main

import "fmt"

type Container[T any] struct {
    value T
}

func (c Container[T]) Get() T {
    return c.value
}

// Generic method
func (c *Container[T]) Map[U any](mapper func(T) U) Container[U] {
    return Container[U]{value: mapper(c.value)}
}

func main() {
    intContainer := Container[int]{value: 42}
    stringContainer := intContainer.Map(func(i int) string {
        return fmt.Sprintf("Number: %d", i)
    })
    
    fmt.Println(stringContainer.Get()) // "Number: 42"
}

Built-in Constraints

Go provides some built-in constraints:

  • any: equivalent to interface{} (any type)
  • comparable: types that support == and !=
  • constraints.Ordered: types that support <, <=, >, >=
package main

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

// Generic sort function
func SortSlice[T constraints.Ordered](slice []T) {
    sort.Slice(slice, func(i, j int) bool {
        return slice[i] < slice[j]
    })
}

func main() {
    ints := []int{3, 1, 4, 1, 5}
    SortSlice(ints)
    fmt.Println(ints) // [1 1 3 4 5]
    
    strings := []string{"zebra", "apple", "banana"}
    SortSlice(strings)
    fmt.Println(strings) // [apple banana zebra]
}

Type Assertions and Type Switches

With generics, you might need to work with interface types:

package main

import "fmt"

// PrintDetails works with any type that can be converted to string
func PrintDetails[T any](value T) {
    switch v := any(value).(type) {
    case string:
        fmt.Printf("String: %s (length: %d)\n", v, len(v))
    case int:
        fmt.Printf("Integer: %d\n", v)
    case fmt.Stringer:
        fmt.Printf("Stringer: %s\n", v.String())
    default:
        fmt.Printf("Other type: %T\n", v)
    }
}

func main() {
    PrintDetails("hello") // String: hello (length: 5)
    PrintDetails(42)      // Integer: 42
}

Generics vs Interfaces

When to use generics vs interfaces:

  • Use generics when you need type safety and performance
  • Use interfaces when you need polymorphism at runtime
// Interface approach
type Printer interface {
    Print()
}

type StringPrinter struct{ value string }
func (s StringPrinter) Print() { fmt.Println(s.value) }

type IntPrinter struct{ value int }
func (i IntPrinter) Print() { fmt.Println(i.value) }

// Generic approach
func Print[T any](value T) {
    fmt.Println(value)
}

Best Practices

  1. Use meaningful type parameter names: T for general types, K and V for key-value pairs
  2. Keep constraints simple: Use the most specific constraint you can
  3. Avoid overusing generics: Sometimes interfaces or concrete types are simpler
  4. Consider performance: Generics have minimal runtime overhead
  5. Document your generics: Explain what types are expected

Common Patterns

Generic Map Function

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

Generic Filter Function

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

Generic Set

type Set[T comparable] map[T]bool

func NewSet[T comparable]() Set[T] {
    return make(Set[T])
}

func (s Set[T]) Add(item T) {
    s[item] = true
}

func (s Set[T]) Contains(item T) bool {
    return s[item]
}

Generics are a powerful addition to Go that can make your code more reusable and type-safe. Start with simple cases and gradually use more advanced features as you become comfortable.

For more information, check the Go generics proposal and the generics tutorial.

If you’re new to Go interfaces, you might want to read about Go interfaces first.

Last updated on