Go Language Generics

Generics is an important feature introduced in Go version 1.18, enabling developers to write more flexible and reusable code.

Generics is mainly implemented through the following two core concepts:

  • Type Parameters:Allows you to use one or more types as parameters in function or type definitions.
  • Type Constraints:Specifies the conditions that type parameters must satisfy, ensuring that these types can be safely operated on inside functions.
Concepts Function Example
Type parameter Declared after the function or type name, representing an unspecified type. [T any]
Type constraint Defines the conditions that type parameters must satisfy (such as supported operators or methods). int,float64,comparable,constraints.Ordered,any
any Constrain type parameters toanytype. [T any]
comparable Constrain type parameters tocomparabletype. [K comparable]

Generics allows us to write code that does not depend on specific data types.

Before the introduction of generics, if we wanted to handle different types of data, we usually had to write duplicate functions for each type.

Limitations of the traditional approach:

Example

// Function to handle int type
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

// Function to handle float64 type
func MaxFloat(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}

Solution using generics:

Example

// A function handles multiple types
func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Detailed Explanation of Generics Syntax

Type Parameter Declaration

Generic functions and types are declared through type parameter lists, with the syntax:[Type parameters Constraints]。

Example

// Basic syntax structure
funcFunction Name[T constraint](parameter T)Return value type{
    // Function body
}

typetype name[T constraint] struct {
    // Struct field
}

Type parameter naming conventions

  • Usually uppercase letters are used:T、K、V、Ewait
  • T: denotes Type
  • K: indicates Key
  • V: indicates Value
  • E: denotes Element

Constraints

Constraints define the conditions that type parameters must satisfy and are a core concept of generics.

Built-in constraints

1. anyconstraint

anyis an empty interfaceinterface{}an alias for, meaning any type is allowed.

Example

func PrintAny[T any](value T) {
    fmt.Printf("Value: %v, Type: %T\n", value, value)
}

// Usage example
PrintAny(42)        // Value: 42, Type: int
PrintAny("hello")   // Value: hello, Type: string
PrintAny(3.14)      // Value: 3.14, Type: float64

2. comparableconstraint

comparableIndicates type support==and!=operator.

Example

func FindIndex[T comparable](slice []T, target T) int {
    for i, v := range slice {
        if v == target {
            return i
        }
    }
    return -1
}

// Usage example
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(FindIndex(numbers, 3))  // Output: 2

names := []string{"Alice", "Bob", "Charlie"}
fmt.Println(FindIndex(names, "Bob"))  // Output: 1

3. Union Constraints

Usage|Operator combines multiple types.

Example

// Numeric type constraint
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

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

// Usage example
fmt.Println(Add(10, 20))        // Output: 30
fmt.Println(Add(3.14, 2.71))    // Output: 5.85

Custom constraint

1. Method constraints

Define constraints that require specific methods.

Example

// Define Stringer constraint
type Stringer interface {
    String() string
}

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

// Implement a custom type
type Person struct {
    Name string
    Age  int
}

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

// Usage example
person := Person{Name: "Alice", Age: 25}
PrintString(person)  // Output: Alice (25 years old)

2. Complex constraints

Combine type and method requirements.

Example

// Requires the type to be numeric and implement String() method
type NumericStringer interface {
    Number
    String() string
}

Generics Function Practice

1. Generic utility functions

Example

// Swap two values
func Swap[T any](a, b T) (T, T) {
    return b, a
}

// Check whether the slice contains an element
func Contains[T comparable](slice []T, target T) bool {
    for _, item := range slice {
        if item == target {
            return true
        }
    }
    return false
}

// Deduplicate function
func Unique[T comparable](slice []T) []T {
    seen := make(map[T]bool)
    result := []T{}
   
    for _, item := range slice {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

// Usage example
func main() {
    // Swap example
    a, b := 10, 20
    a, b = Swap(a, b)
    fmt.Printf("a=%d, b=%d\n", a, b)  // Output: a=20, b=10
   
    // Contains example
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(Contains(numbers, 3))  // Output: true
   
    // Unique example
    duplicates := []int{1, 2, 2, 3, 4, 4, 5}
    unique := Unique(duplicates)
    fmt.Println(unique)  // Output: [1 2 3 4 5]
}

2. Mathematical operation functions

Example

// Find the maximum value of a slice
func Max[T Number](slice []T) T {
    if len(slice) == 0 {
        var zero T
        return zero
    }
   
    max := slice[0]
    for _, value := range slice[1:] {
        if value > max {
            max = value
        }
    }
    return max
}

// Find the minimum value of a slice
func Min[T Number](slice []T) T {
    if len(slice) == 0 {
        var zero T
        return zero
    }
   
    min := slice[0]
    for _, value := range slice[1:] {
        if value < min {
            min = value
        }
    }
    return min
}

// Find the average value of a slice
func Average[T Number](slice []T) float64 {
    if len(slice) == 0 {
        return 0
    }
   
    var sum T
    for _, value := range slice {
        sum += value
    }
    return float64(sum) / float64(len(slice))
}

// Usage example
func main() {
    ints := []int{1, 5, 3, 9, 2}
    floats := []float64{1.1, 5.5, 3.3, 9.9, 2.2}
   
    fmt.Printf("Max int: %d\n", Max(ints))           // Output: 9
    fmt.Printf("Min float: %.1f\n", Min(floats))     // Output: 1.1
    fmt.Printf("Average: %.2f\n", Average(floats))   // Output: 4.40
}

Generic types

1. Generic structs

Example

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

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

// Pop
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }
   
    lastIndex := len(s.elements) - 1
    value := s.elements[lastIndex]
    s.elements = s.elements[:lastIndex]
    return value, true
}

// Peek at the top element
func (s *Stack[T]) Peek() (T, bool) {
    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }
    return s.elements[len(s.elements)-1], true
}

// Check if the stack is empty
func (s *Stack[T]) IsEmpty() bool {
    return len(s.elements) == 0
}

// Usage example
func main() {
    // Integer stack
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)
   
    fmt.Println(intStack.Pop())  // Output: 3 true
   
    // String stack
    stringStack := Stack[string]{}
    stringStack.Push("hello")
    stringStack.Push("world")
   
    fmt.Println(stringStack.Pop())  // Output: world true
}

2. Generic map (Map)

Example

// Thread-safe generic map
type SafeMap[K comparable, V any] struct {
    data map[K]V
    mutex sync.RWMutex
}

// Create a new SafeMap
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
    return &SafeMap[K, V]{
        data: make(map[K]V),
    }
}

// Set key-value pair
func (m *SafeMap[K, V]) Set(key K, value V) {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    m.data[key] = value
}

// Get value
func (m *SafeMap[K, V]) Get(key K) (V, bool) {
    m.mutex.RLock()
    defer m.mutex.RUnlock()
    value, exists := m.data[key]
    return value, exists
}

// Delete key
func (m *SafeMap[K, V]) Delete(key K) {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    delete(m.data, key)
}

// Get all keys
func (m *SafeMap[K, V]) Keys() []K {
    m.mutex.RLock()
    defer m.mutex.RUnlock()
   
    keys := make([]K, 0, len(m.data))
    for key := range m.data {
        keys = append(keys, key)
    }
    return keys
}

// Usage example
func main() {
    // Create a string-to-integer map
    scores := NewSafeMap[string, int]()
    scores.Set("Alice", 95)
    scores.Set("Bob", 87)
   
    if score, exists := scores.Get("Alice"); exists {
        fmt.Printf("Alice's score: %d\n", score)  // Output: Alice's score: 95
    }
   
    fmt.Println("Keys:", scores.Keys())  // Output: Keys: [Alice Bob]
}

Type inference

The Go compiler can automatically infer type parameters, making code more concise.

Example

// No need to explicitly specify the type
func main() {
    // Type inference example
    fmt.Println(Max([]int{1, 2, 3}))        // The compiler infers T as int
    fmt.Println(Max([]float64{1.1, 2.2}))   // The compiler infers T as float64
   
    // Explicitly specify the type (sometimes necessary)
    var result int = Max[int]([]int{1, 2, 3})
    fmt.Println(result)
}

Practice exercises

Exercise 1: Implement a generic filter

Write aFilterfunction that filters slice elements based on a condition.

Example

// Your implementation code goes here
func Filter[T any](slice []T, predicate func(T) bool) []T {
    // Implement filtering logic
}

// Test code
func main() {
    numbers := []int{1, 2, 3, 4, 5, 6}
    even := Filter(numbers, func(n int) bool {
        return n%2 == 0
    })
    fmt.Println(even)  // Should output: [2 4 6]
}

Exercise 2: Implement a generic map function

Write aMapA function that converts each element in a slice to another type.

Example

// Your implementation code goes here
func Map[T any, U any](slice []T, mapper func(T) U) []U {
    // Implement mapping logic
}

// Test code
func main() {
    numbers := []int{1, 2, 3, 4, 5}
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("Number: %d", n)
    })
    fmt.Println(strings)
}

Common issues and considerations

1. Performance considerations

Generics perform type specialization at compile time, with runtime performance comparable to hand-written specific-type code.

2. Choice of type constraints

  • Usageanyis most flexible when, but limited in functionality.
  • UsagecomparableSupports equality comparison
  • Use union constraints to restrict the available concrete types.

3. Error handling

Example

// Good error handling practices
func SafeMax[T Number](slice []T) (T, error) {
    if len(slice) == 0 {
        var zero T
        return zero, errors.New("slice is empty")
    }
    return Max(slice), nil
}
other extensions