Skip to content

Generics

Generics in Go

Generics were introduced in Go 1.18 to enable functions and types to operate on multiple types while maintaining type safety. Generics provide a way to write reusable, type-agnostic code without sacrificing the performance and simplicity Go is known for.


1. Basics of Generics

Generics in Go are implemented using type parameters. A type parameter is a placeholder for a type that is provided when the function or type is instantiated.

Example: Generic Function

package main

import "fmt"

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

func main() {
    Print(42)
    Print("Hello, Generics!")
    Print(3.14)
}
  • [T any]: Declares a type parameter T that can be any type.

2. Type Constraints

You can constrain type parameters to ensure they satisfy specific behaviors or methods. Constraints are defined using interfaces.

Example: Constrained Generics

package main

import (
    "fmt"
)

// Constrain T to numeric types
type Numeric interface {
    int | int8 | int16 | int32 | int64 | float32 | float64
}

func Add[T Numeric](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(Add(5, 10))       // Works with int
    fmt.Println(Add(3.14, 2.71)) // Works with float64
}

3. Generic Types

Structs and other types can also use type parameters.

Example: Generic Struct

package main

import "fmt"

// Generic struct
type Stack[T any] struct {
    elements []T
}

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

func (s *Stack[T]) Pop() T {
    if len(s.elements) == 0 {
        panic("Stack is empty")
    }
    last := s.elements[len(s.elements)-1]
    s.elements = s.elements[:len(s.elements)-1]
    return last
}

func main() {
    var intStack Stack[int]
    intStack.Push(10)
    intStack.Push(20)
    fmt.Println(intStack.Pop()) // Output: 20
    fmt.Println(intStack.Pop()) // Output: 10

    var stringStack Stack[string]
    stringStack.Push("Go")
    stringStack.Push("Generics")
    fmt.Println(stringStack.Pop()) // Output: Generics
}

4. Multiple Type Parameters

You can use multiple type parameters to generalize more complex behaviors.

Example: Multiple Type Parameters

package main

import "fmt"

func Combine[K, V any](key K, value V) (K, V) {
    return key, value
}

func main() {
    k, v := Combine("ID", 12345)
    fmt.Printf("Key: %v, Value: %v\n", k, v)
}

5. Constraints with Methods

If a constraint interface defines methods, the type parameter must implement them.

Example: Constraints with Methods

package main

import "fmt"

// Constrain to types implementing fmt.Stringer
type Stringer interface {
    String() string
}

func PrintString[T Stringer](value T) {
    fmt.Println(value.String())
}

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("Name: %s, Age: %d", p.Name, p.Age)
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    PrintString(p)
}

6. Standard Library Support

Generics are integrated into the Go standard library. For example, constraints is a package that provides ready-made constraints.

Common Constraints:

  • constraints.Ordered: For types that support <, >, <=, >=.

Example: Sorting with Ordered Constraint

package main

import (
    "constraints"
    "fmt"
)

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

func main() {
    fmt.Println(Min(10, 20))       // Works with integers
    fmt.Println(Min(3.14, 2.71))  // Works with floats
    fmt.Println(Min("a", "b"))    // Works with strings
}

7. Generic Maps and Slices

Generics allow building reusable utilities for common operations on collections like maps and slices.

Example: Filter a Slice

package main

import "fmt"

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

func main() {
    nums := []int{1, 2, 3, 4, 5}
    even := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(even) // Output: [2 4]
}

8. Generics and Performance

While generics provide flexibility, they may introduce slight performance overhead due to type instantiations. However, Go's compiler optimizes generic code, so the difference is usually negligible.


Best Practices with Generics

  1. Use When Necessary: Avoid overusing generics; use them when they simplify your code or reduce duplication.
  2. Leverage Constraints: Use meaningful constraints to guide type usage.
  3. Keep It Readable: Overly complex generic declarations can harm readability.
  4. Stick to Go’s Simplicity: Generics should enhance, not replace, Go's idiomatic patterns.

Additional Topics to Explore

  • Generic Interface Constraints
  • Comparisons Between Generics and Reflection
  • Advanced Patterns like Generics with Recursive Types