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 Name | Description | Example Usage | Source |
|---|---|---|---|
any | Type parameter can be any type. | Used for linked list nodes element[T any]. | |
comparable | Type parameter must support equality comparison (e.g., == or !=). | Used for map key types K. | |
cmp.Ordered | Type 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:
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:
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.
// 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.
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.
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.
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.
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)
Define Data Structures: Define
UserandActionstructs.gotype User struct { ... } // type Action struct { ... } //Define Union Interface:
APIDataconstrains type parameter T to be eitherUserorAction.gotype APIData interface { User | Action } //Define Generic Struct:
APIResponseusesAPIDatato constrain type parameterT.gotype 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)
Define Underlying Interface:
IntLikematches all types with underlying typeint.gotype IntLike interface { ~int } //Define Custom Types: Define
MyIntandYourIntbased onint.gotype MyInt int // type YourInt MyInt //Define Generic Function:
Addfunction usesIntLikeconstraint.gofunc 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.
var a MyInt = 10
var b MyInt = 20
// MyInt addition: 30
Add(a, b) //