avatar🌌
TangentTangent Blog

技术博客 | Go & Kubernetes & Linux

技术博客 | Go & Kubernetes & Linux

Go Generics Usage Tutorial

Go generics introduce type parameters and constraints, allowing code to work with multiple types, thereby improving code reusability and type safety.

1. Generic Type Parameters and Built-in Constraints ​

When defining generic functions or structs, type parameters are placed in square brackets []. Go provides several important built-in constraints:

Constraint NameDescriptionExample UsageSource
anyType parameter can be any type.Used for linked list nodes element[T any].
comparableType parameter must support equality comparison (e.g., == or !=).Used for map key types K.
cmp.OrderedType parameter must support ordering comparison (e.g., <, >), commonly used for sorting and finding minimum values.Used for comparison functions GMin[T cmp.Ordered].

1.1 Generic Functions Using Built-in Constraints ​

GMin Function (Find Minimum Value)

Uses the cmp.Ordered constraint to ensure type T supports the < operator:

go
func GMin[T cmp.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}
// Example: GMin(1, 2)

Keys Function (Get Map Keys)

Uses the comparable constraint for K as the map key type, and any constraint for V as the map value type:

go
func Keys[K comparable, V any](m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}

2. Generic Data Structures: Singly Linked List List ​

Generic structs allow creating universal containers that can store different types of data.

2.1 Struct Definition ​

Both the List struct and its node element use [T any] as type parameters, indicating they can store values of any type.

go
// element is a node in the linked list, also a generic type.
type element[T any] struct {
    val T
    next *element[T]
}

// List is a generic singly linked list. It can store values of any type.
type List[T any] struct {
    head, tail *element[T]
    length int
}

2.2 Generic Method Implementation ​

Push Method (Add Element to Tail)

The Push method receives a value v of type parameter T.

go
func (l *List[T]) Push(v T) {
    e := &element[T]{val: v}
    if l.tail != nil {
        l.tail.next = e
        l.tail = e
    } else {
        l.head = e
        l.tail = e
    }
    l.length++
}

Pop Method (Remove Tail Element)

The Pop method returns a value v of type parameter T. This method needs to handle cases where the list is empty, has one element, or multiple elements.

go
func (l *List[T]) Pop() (v T, ok bool) {
    if l.head == nil { // List is empty
        return
    }
    
    if l.head == l.tail { // Only one element
        v = l.head.val
        l.head = nil
        l.tail = nil
    } else {
        p := l.head
        for p.next != l.tail { // Find the second-to-last node
            p = p.next
        }
        v = l.tail.val
        p.next = nil
        l.tail = p
    }

    l.length--
    ok = true
    return
}

3. Generic Functions: Map and Filter ​

Generic functions can implement common collection operations, applicable to slices of different types.

3.1 Filter Function ​

The Filter function takes a slice of type T and filters elements based on a predicate function that returns a boolean, returning a new slice of elements that meet the condition.

go
func Filter[T any](collection []T, predicate func(item T, index int) bool) []T {
    result := make([]T, 0, len(collection))
    for i, item := range collection {
        if predicate(item, i) {
            result = append(result, item)
        }
    }
    return result
}

3.2 Map Function ​

The Map function uses two type parameters T and U, transforming a slice of type T into a new slice of type U through the iteratee function.

go
func Map[T any, U any](collection []T, iteratee func(item T) U) []U {
    result := make([]U, len(collection))
    for i, item := range collection {
        result[i] = iteratee(item)
    }
    return result
}

4. Custom Constraints (Interface Constraints) ​

When built-in constraints are insufficient, interfaces can be used to define custom constraints, limiting the type parameters to specific type sets or underlying structures.

4.1 Union Type Constraints ​

Union type constraints use the | operator to combine multiple concrete types, restricting the type parameter to be one of these types.

Example: API Response Data Constraint (APIData)

  1. Define Data Structures: Define User and Action structs.

    go
    type User struct { ... } //
    type Action struct { ... } //
  2. Define Union Interface: APIData constrains type parameter T to be either User or Action.

    go
    type APIData interface {
        User | Action
    } //
  3. Define Generic Struct: APIResponse uses APIData to constrain type parameter T.

    go
    type APIResponse[T APIData] struct {
        Code int    `json:"code"` //
        Msg string `json:"msg"` //
        Data T      `json:"data"` //
    }

Usage Example: When parsing API responses, by specifying concrete types (e.g., APIResponse[User] or APIResponse[Action]), type safety of the Data field can be ensured.

4.2 Underlying Type Constraints ​

Using the ~ symbol defines underlying type constraints, allowing type parameters to match all types with a specific underlying type (including basic types and custom types).

Example: Integer-like Type Constraint (IntLike)

  1. Define Underlying Interface: IntLike matches all types with underlying type int.

    go
    type IntLike interface {
        ~int
    } //
  2. Define Custom Types: Define MyInt and YourInt based on int.

    go
    type MyInt int //
    type YourInt MyInt //
  3. Define Generic Function: Add function uses IntLike constraint.

    go
    func Add[T IntLike](a, b T) T {
        return a + b
    } //

Usage Example: The Add function can accept built-in int type, as well as custom types MyInt and YourInt.

go
var a MyInt = 10
var b MyInt = 20
// MyInt addition: 30
Add(a, b) //
Kubernetes Secret Explained
Overview: KEDA Scale to Zero
由 Valaxy v1.2.2 驱动|主题-Yunv1.2.2