Generics
Generics in Go (introduced in Go 1.18) allow you to define functions or types that can operate on any data type while maintaining strong type-checking. This eliminates the need for boilerplate code or using the interface type, which lacks compile-time safety.
Key Concepts
Type Parameters
func PrintSlice[T any](slice []T) {
    for _, v := range slice {
        fmt.Println(v)
    }
}
- Generics use type parameters in square brackets 
[]to specify the types a function or type can accept. - Here, T is a type parameter that can represent any type.
 
Constraints
func Add[T int | float64](a, b T) T {
    return a + b
}
- Constraints define what operations are allowed on type parameters.
 - Here, T is constrained to int or float64, ensuring the + operator is valid.
 
any Constraint
- The 
anykeyword is an alias forinterface{}and represents any type without restrictions. 
Benefits
- Compile-Time Safety: The Go compiler enforces type correctness at compile time. Ensures compile-time checks, reducing runtime errors.
 - Monomorphization: For performance, the compiler generates specific versions of generic functions/types for each type used. This avoids runtime overhead.
 - Code Reusability: Avoids duplication of similar functions or data structures.
 - Performance: Unlike 
interface{}, generics don’t require type assertions or reflection. 
Examples
Generic Functions
A function that works with any type
package main
import "fmt"
func PrintSlice[T any](slice []T) {
    for _, v := range slice {
        fmt.Println(v)
    }
}
func main() {
    PrintSlice([]int{1, 2, 3})
    PrintSlice([]string{"a", "b", "c"})
}
Output:
1
2
3
a
b
c
Generic Types
Define a data structure with type parameters:
package main
import "fmt"
// Stack is a generic stack
type Stack[T any] struct {
    items []T
}
func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() T {
    last := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return last
}
func main() {
    s1 := Stack[int]{}
    s1.Push(10)
    s1.Push(20)
    fmt.Println(s1.Pop()) // 20
    fmt.Println(s1.Pop()) // 10
    s2 := Stack[string]{}
    s2.Push("A")
    s2.Push("B")
    fmt.Println(s2.Pop()) // B
    fmt.Println(s2.Pop()) // A
}
Output:
20
10
B
A
- In above example we can create a stack that work with any types.
 - This is safer rather than using 
interface{}because the type is forced in compile time instead of casting it manually in running time. 
Multiple Type Parameters
Functions or types with more than one type parameter:
package main
import "fmt"
func Combine[K comparable, V any](keys []K, values []V) map[K]V {
    result := make(map[K]V)
    for i, key := range keys {
        result[key] = values[i]
    }
    return result
}
func main() {
    keys := []string{"a", "b", "c"}
    values := []int{1, 2, 3}
    fmt.Println(Combine(keys, values))
    // switch them up
    fmt.Println(Combine(values, keys))
}
Output:
map[a:1 b:2 c:3]
map[1:a 2:b 3:c]
- In above example we create a generic map that can accept 
anytype as it values. - Note that we use 
comparableinstead ofanybecausemapin golang need this to avoids ambiguity, inefficiency, and potential bugs related to mutable or non-comparable types. 
New Constraints
Restricting the type parameters with constraints:
package main
import "fmt"
type Number interface {
    int | int64 | float64
}
func Sum[T Number](a, b T) T {
    return a + b
}
func main() {
    fmt.Println(Sum(1, 1))
    fmt.Println(Sum(1.1, 1.1))
}
Output:
2
2.2
- 
In above example we restrict function
Sumto only acceptNumbertype that we defined as eitherint,int64, orfloat64. - 
If we tried to input string as parameter
Sum("A", "B")it will return error:string does not satisfy Number (string missing in int | int64 | float64)