YouTip LogoYouTip

Go Generics

Generics are an important feature introduced in Go version 1.18, allowing developers to write more flexible and reusable code. Generics are mainly implemented through the following two core concepts: * **Type Parameters:** Allow you to use one or more types as parameters in function or type definitions. * **Type Constraints:** Specify conditions that type parameters must satisfy, ensuring safe operations on these types within functions. | Concept | Purpose | Example | | --- | --- | --- | | **Type Parameters** | Declared after function or type names, representing pending types. | `` | | **Type Constraints** | Define conditions that type parameters must satisfy (such as supported operators or methods). | `int, float64, comparable, constraints.Ordered, any` | | **`any`** | Constrains the type parameter to **any** type. | `` | | **`comparable`** | Constrains the type parameter to **comparable** types. | `` | Generics allow us to write code that does not depend on specific data types. Before generics were introduced, if we wanted to handle different types of data, we typically needed to write repetitive functions for each type. **Limitations of the Traditional Approach:** ## Example // Function handling int type func MaxInt(a, b int) int { if a > b { return a } return b } // Function handling float64 type func MaxFloat(a, b float64) float64 { if a > b { return a } return b } **Solution Using Generics:** ## Example // One function handling multiple types func Max(a, b T) T { if a > b { return a } return b } * * * ## Generics Syntax in Detail ### Type Parameter Declaration Generic functions and types are declared through type parameter lists, with the syntax ``. ## Example // Basic syntax structure func FunctionName(parameter T) ReturnType { // Function body } type TypeName struct { // Struct fields } ### Type Parameter Naming Conventions * Typically use uppercase letters: `T`, `K`, `V`, `E`, etc. * `T`: Represents Type * `K`: Represents Key * `V`: Represents Value * `E`: Represents Element * * * ## Constraints Constraints define conditions that type parameters must satisfy and are a core concept of generics. ### Built-in Constraints #### 1. `any` Constraint `any` is an alias for the empty interface `interface{}`, meaning any type is acceptable. ## Example func PrintAny(value T) { fmt.Printf("Value: %v, Type: %T\n", value, value) } // Usage examples PrintAny(42) // Value: 42, Type: int PrintAny("hello") // Value: hello, Type: string PrintAny(3.14) // Value: 3.14, Type: float64 #### 2. `comparable` Constraint `comparable` indicates types that support the `==` and `!=` operators. ## Example func FindIndex(slice []T, target T) int { for i, v := range slice { if v == target { return i } } return -1 } // Usage examples 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 Use the `|` operator to combine multiple types. ## Example // Numeric type constraint type Number interface { int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 } func Add(a, b T) T { return a + b } // Usage examples fmt.Println(Add(10, 20)) // Output: 30 fmt.Println(Add(3.14, 2.71)) // Output: 5.85 ### Custom Constraints #### 1. Method Constraints Define constraints that require specific methods. ## Example // Define Stringer constraint type Stringer interface { String() string } func PrintString(value T) { fmt.Println(value.String()) } // Implement 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 // Require type to be numeric and implement String() method type NumericStringer interface { Number String() string } * * * ## Generic Functions in Practice ### 1. General Utility Functions ## Example // Swap two values func Swap(a, b T) (T, T) { return b, a } // Check if slice contains element func Contains(slice []T, target T) bool { for _, item := range slice { if item == target { return true } } return false } // Deduplication function func Unique(slice []T) []T { seen := make(mapbool) result := []T{} for _, item := range slice { if !seen { seen = true result = append(result, item) } } return result } // Usage examples 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: } ### 2. Mathematical Operation Functions ## Example // Find maximum value in slice func Max(slice []T) T { if len(slice) == 0 { var zero T return zero } max := slice for _, value := range slice[1:] { if value > max { max = value } } return max } // Find minimum value in slice func Min(slice []T) T { if len(slice) == 0 { var zero T return zero } min := slice for _, value := range slice[1:] { if value < min { min = value } } return min } // Calculate average of slice func Average(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 examples 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 struct { elements []T } // Push to stack func (s *Stack) Push(value T) { s.elements = append(s.elements, value) } // Pop from stack func (s *Stack) Pop() (T, bool) { if len(s.elements) == 0 { var zero T return zero, false } lastIndex := len(s.elements) - 1 value := s.elements s.elements = s.elements[:lastIndex] return value, true } // Peek at top element func (s *Stack) Peek() (T, bool) { if len(s.elements) == 0 { var zero T return zero, false } return s.elements[len(s.elements)-1], true } // Check if stack is empty func (s *Stack) IsEmpty() bool { return len(s.elements) == 0 } // Usage examples func main() { // Integer stack intStack := Stack{} intStack.Push(1) intStack.Push(2) intStack.Push(3) fmt.Println(intStack.Pop()) // Output: 3 true // String stack stringStack := Stack{} stringStack.Push("hello") stringStack.Push("world") fmt.Println(stringStack.Pop()) // Output: world true } ### 2. Generic Maps ## Example // Thread-safe generic map type SafeMap[K comparable, V any] struct { data mapV mutex sync.RWMutex } // Create new SafeMap func NewSafeMap[K comparable, V any]() *SafeMap[K, V] { return &SafeMap[K, V]{ data: make(mapV), } } // Set key-value pair func (m *SafeMap[K, V]) Set(key K, value V) { m.mutex.Lock() defer m.mutex.Unlock() m.data = value } // Get value func (m *SafeMap[K, V]) Get(key K) (V, bool) { m.mutex.RLock() defer m.mutex.RUnlock() value, exists := m.data 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 examples func main() { // Create string to int 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: } * * * ## Type Inference The Go compiler can automatically infer type parameters, making code more concise. ## Example // No need to explicitly specify types func main() { // Type inference examples fmt.Println(Max([]int{1, 2, 3})) // Compiler infers T as int fmt.Println(Max([]float64{1.1, 2.2})) // Compiler infers T as float64 // Explicitly specify type (sometimes needed) var result int = Max([]int{1, 2, 3}) fmt.Println(result) } * * * ## Practice Exercises ### Exercise 1: Implement Generic Filter Write a `Filter` function to filter slice elements based on a condition. ## Example // Your implementation here func Filter(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: } ### Exercise 2: Implement Generic Map Function Write a `Map` function to transform each element in a slice to another type. ## Example // Your implementation 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 type-specific code. ### 2. Choosing Type Constraints * Using `any` is the most flexible but has limited functionality * Using `comparable` supports equality comparisons * Using union constraints limits the available concrete types ### 3. Error Handling ## Example // Good error handling practice func SafeMax(slice []T) (T, error) { if len(slice) == 0 { var zero T return zero, errors.New("slice is empty") } return Max(slice), nil }
← Csharp Null ConditionRust Operators β†’