Generics in Go: Type Parameters, Constraints & Collections 🧬
Introduced in Go 1.18, Generics enable you to write reusable functions and data structures that work across multiple types while preserving full compile-time type safety.
1. Generic Functions: The Type Parameter Syntax
Type parameters are declared inside square brackets [T any] after the function name:
package main
import "fmt"
// Map applies a transform function to every element in a slice
func Map[T any, R any](items []T, transform func(T) R) []R {
result := make([]R, len(items))
for i, item := range items {
result[i] = transform(item)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4}
// 1. Transform int to string
strings := Map(numbers, func(n int) string {
return fmt.Sprintf("#%d", n)
})
fmt.Println("Strings:", strings) // [#1 #2 #3 #4]
// 2. Transform int to bool
isEven := Map(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println("IsEven:", isEven) // [false true false true]
}2. Type Constraints: Restricting Types
If you use any, you can only perform operations valid for all types (like assignment and printing). If you want to compare values using < or ==, you need Constraints:
1. comparable (Built-in for == and !=)
Used whenever you need to compare values or use T as a map key:
// Contains checks if an item exists in a slice
func Contains[T comparable](slice []T, target T) bool {
for _, item := range slice {
if item == target {
return true
}
}
return false
}2. cmp.Ordered (For <, <=, >, >=)
Provided by the standard library package cmp (Go 1.21+):
package main
import (
"cmp"
"fmt"
)
// Min returns the smaller of two ordered values
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(10, 20)) // 10 (int)
fmt.Println(Min(3.14, 2.71)) // 2.71 (float64)
fmt.Println(Min("zebra", "apple")) // "apple" (string)
}3. Generic Data Structures: A Thread-Safe Set
Generics allow building reusable collections without using map[string]struct{} everywhere:
package main
import (
"fmt"
"sync"
)
// Set is a generic thread-safe collection of unique items
type Set[T comparable] struct {
mu sync.RWMutex
items map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{
items: make(map[T]struct{}),
}
}
func (s *Set[T]) Add(val T) {
s.mu.Lock()
defer s.mu.Unlock()
s.items[val] = struct{}{}
}
func (s *Set[T]) Has(val T) bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, exists := s.items[val]
return exists
}
func main() {
intSet := NewSet[int]()
intSet.Add(42)
fmt.Println("Has 42:", intSet.Has(42)) // true
fmt.Println("Has 99:", intSet.Has(99)) // false
stringSet := NewSet[string]()
stringSet.Add("kubernetes")
fmt.Println("Has k8s:", stringSet.Has("kubernetes")) // true
}4. Custom Constraints with Union Syntax (| and ~)
You can create your own constraints using interface union syntax:
package mathutil
// Number accepts any standard int or float, plus custom types derived from them (~)
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](vals []T) T {
var total T
for _, v := range vals {
total += v
}
return total
}The tilde ~int means: “any type whose underlying type is int” (e.g., type UserID int).
5. Generic Sorting (slices.Sort and slices.SortFunc)
Go 1.21 replaced legacy sort.Interface boilerplate with the type-safe generic slices package.
package main
import (
"cmp"
"fmt"
"slices"
)
type Employee struct {
Name string
Age int
}
func main() {
// 1. Natural sorting on ordered types
numbers := []int{50, 10, 40, 20}
slices.Sort(numbers)
fmt.Println("Sorted numbers:", numbers) // [10 20 40 50]
// 2. Custom sorting using slices.SortFunc and cmp.Compare
team := []Employee{
{"Alice", 34},
{"Bob", 23},
{"Charlie", 29},
}
// Sort by age ascending
slices.SortFunc(team, func(a, b Employee) int {
return cmp.Compare(a.Age, b.Age)
})
fmt.Printf("Sorted by age: %+v\n", team)
// Binary search on sorted slice
idx, found := slices.BinarySearch(numbers, 20)
fmt.Printf("BinarySearch 20: found=%t at index=%d\n", found, idx)
}6. Range over Function Iterators (iter.Seq in Go 1.23+)
Go 1.23 introduced native compiler support for ranging over function iterators (iter.Seq[V] and iter.Seq2[K, V]).
package main
import (
"fmt"
"iter"
)
// Backward returns an iterator yielding items in reverse order
func Backward[E any](s []E) iter.Seq2[int, E] {
return func(yield func(int, E) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return // early exit if loop breaks
}
}
}
}
func main() {
names := []string{"zero", "one", "two", "three"}
// Native 'for ... range' over a custom iterator function
for idx, val := range Backward(names) {
fmt.Printf("Index %d: %s\n", idx, val)
if idx == 2 {
break // yield returns false, terminating Backward cleanly
}
}
}Across the Wiki
- Generics Basics — Language-independent concepts and comparisons.
- Slices & Maps — Standard Go collection types.
- Type Assertions — When to use generics vs interfaces.