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
}
YouTip