Generics
Generics in Go
Generics were introduced in Go 1.18 to enable functions and types to operate on multiple types while maintaining type safety. Generics provide a way to write reusable, type-agnostic code without sacrificing the performance and simplicity Go is known for.
1. Basics of Generics
Generics in Go are implemented using type parameters. A type parameter is a placeholder for a type that is provided when the function or type is instantiated.
Example: Generic Function
package main
import "fmt"
// Generic function with a type parameter T
func Print[T any](value T) {
fmt.Println(value)
}
func main() {
Print(42)
Print("Hello, Generics!")
Print(3.14)
}
[T any]
: Declares a type parameterT
that can be any type.
2. Type Constraints
You can constrain type parameters to ensure they satisfy specific behaviors or methods. Constraints are defined using interfaces.
Example: Constrained Generics
package main
import (
"fmt"
)
// Constrain T to numeric types
type Numeric interface {
int | int8 | int16 | int32 | int64 | float32 | float64
}
func Add[T Numeric](a, b T) T {
return a + b
}
func main() {
fmt.Println(Add(5, 10)) // Works with int
fmt.Println(Add(3.14, 2.71)) // Works with float64
}
3. Generic Types
Structs and other types can also use type parameters.
Example: Generic Struct
package main
import "fmt"
// Generic struct
type Stack[T any] struct {
elements []T
}
func (s *Stack[T]) Push(value T) {
s.elements = append(s.elements, value)
}
func (s *Stack[T]) Pop() T {
if len(s.elements) == 0 {
panic("Stack is empty")
}
last := s.elements[len(s.elements)-1]
s.elements = s.elements[:len(s.elements)-1]
return last
}
func main() {
var intStack Stack[int]
intStack.Push(10)
intStack.Push(20)
fmt.Println(intStack.Pop()) // Output: 20
fmt.Println(intStack.Pop()) // Output: 10
var stringStack Stack[string]
stringStack.Push("Go")
stringStack.Push("Generics")
fmt.Println(stringStack.Pop()) // Output: Generics
}
4. Multiple Type Parameters
You can use multiple type parameters to generalize more complex behaviors.
Example: Multiple Type Parameters
package main
import "fmt"
func Combine[K, V any](key K, value V) (K, V) {
return key, value
}
func main() {
k, v := Combine("ID", 12345)
fmt.Printf("Key: %v, Value: %v\n", k, v)
}
5. Constraints with Methods
If a constraint interface defines methods, the type parameter must implement them.
Example: Constraints with Methods
package main
import "fmt"
// Constrain to types implementing fmt.Stringer
type Stringer interface {
String() string
}
func PrintString[T Stringer](value T) {
fmt.Println(value.String())
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("Name: %s, Age: %d", p.Name, p.Age)
}
func main() {
p := Person{Name: "Alice", Age: 30}
PrintString(p)
}
6. Standard Library Support
Generics are integrated into the Go standard library. For example, constraints
is a package that provides ready-made constraints.
Common Constraints:
constraints.Ordered
: For types that support<
,>
,<=
,>=
.
Example: Sorting with Ordered Constraint
package main
import (
"constraints"
"fmt"
)
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(10, 20)) // Works with integers
fmt.Println(Min(3.14, 2.71)) // Works with floats
fmt.Println(Min("a", "b")) // Works with strings
}
7. Generic Maps and Slices
Generics allow building reusable utilities for common operations on collections like maps and slices.
Example: Filter a Slice
package main
import "fmt"
func Filter[T any](data []T, predicate func(T) bool) []T {
var result []T
for _, v := range data {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
even := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(even) // Output: [2 4]
}
8. Generics and Performance
While generics provide flexibility, they may introduce slight performance overhead due to type instantiations. However, Go's compiler optimizes generic code, so the difference is usually negligible.
Best Practices with Generics
- Use When Necessary: Avoid overusing generics; use them when they simplify your code or reduce duplication.
- Leverage Constraints: Use meaningful constraints to guide type usage.
- Keep It Readable: Overly complex generic declarations can harm readability.
- Stick to Go’s Simplicity: Generics should enhance, not replace, Go's idiomatic patterns.
Additional Topics to Explore
- Generic Interface Constraints
- Comparisons Between Generics and Reflection
- Advanced Patterns like Generics with Recursive Types