Skip to main content

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 for interface{} 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 of any because map 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 accept Number type that we defined as either int, int64, or float64.

  • 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)