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
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
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
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 TypeK: indicates KeyV: indicates ValueE: 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
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
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
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
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
type NumericStringer interface {
Number
String() string
}
Generics Function Practice
1. Generic utility functions
Example
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
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
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
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
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
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
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
- Usage
anyis most flexible when, but limited in functionality. - Usage
comparableSupports equality comparison - Use union constraints to restrict the available concrete types.
3. Error handling
Example
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
}