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
any
keyword 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
any
type as it values. - Note that we use
comparable
instead ofany
becausemap
in 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
Sum
to only acceptNumber
type 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)