Generics in Go
Generics, introduced in Go 1.18, allow you to write functions and types that work with multiple types while maintaining type safety. This feature helps reduce code duplication and makes your code more flexible.
Generic Functions
A generic function can work with different types. Use square brackets to define type parameters:
package main
import "fmt"
// Generic function that works with any comparable type
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
func main() {
ints := []int{1, 2, 3, 4, 5}
fmt.Println(Contains(ints, 3)) // true
strings := []string{"apple", "banana", "cherry"}
fmt.Println(Contains(strings, "banana")) // true
}The T is a type parameter that gets replaced with the actual type when you call the function.
Type Constraints
You can constrain type parameters to certain behaviors using interfaces:
package main
import "fmt"
// Number interface for numeric types
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
// Generic sum function for numbers
func Sum[T Number](numbers []T) T {
var total T
for _, num := range numbers {
total += num
}
return total
}
func main() {
ints := []int{1, 2, 3, 4, 5}
fmt.Println(Sum(ints)) // 15
floats := []float64{1.1, 2.2, 3.3}
fmt.Println(Sum(floats)) // 6.6
}Generic Types
You can define generic types as well:
package main
import "fmt"
// 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 {
if len(s.items) == 0 {
panic("stack is empty")
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item
}
func (s *Stack[T]) IsEmpty() bool {
return len(s.items) == 0
}
func main() {
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
fmt.Println(intStack.Pop()) // 2
stringStack := Stack[string]{}
stringStack.Push("hello")
stringStack.Push("world")
fmt.Println(stringStack.Pop()) // "world"
}Generic Methods
Methods can also be generic:
package main
import "fmt"
type Container[T any] struct {
value T
}
func (c Container[T]) Get() T {
return c.value
}
// Generic method
func (c *Container[T]) Map[U any](mapper func(T) U) Container[U] {
return Container[U]{value: mapper(c.value)}
}
func main() {
intContainer := Container[int]{value: 42}
stringContainer := intContainer.Map(func(i int) string {
return fmt.Sprintf("Number: %d", i)
})
fmt.Println(stringContainer.Get()) // "Number: 42"
}Built-in Constraints
Go provides some built-in constraints:
any: equivalent tointerface{}(any type)comparable: types that support == and !=constraints.Ordered: types that support <, <=, >, >=
package main
import (
"fmt"
"sort"
"golang.org/x/exp/constraints"
)
// Generic sort function
func SortSlice[T constraints.Ordered](slice []T) {
sort.Slice(slice, func(i, j int) bool {
return slice[i] < slice[j]
})
}
func main() {
ints := []int{3, 1, 4, 1, 5}
SortSlice(ints)
fmt.Println(ints) // [1 1 3 4 5]
strings := []string{"zebra", "apple", "banana"}
SortSlice(strings)
fmt.Println(strings) // [apple banana zebra]
}Type Assertions and Type Switches
With generics, you might need to work with interface types:
package main
import "fmt"
// PrintDetails works with any type that can be converted to string
func PrintDetails[T any](value T) {
switch v := any(value).(type) {
case string:
fmt.Printf("String: %s (length: %d)\n", v, len(v))
case int:
fmt.Printf("Integer: %d\n", v)
case fmt.Stringer:
fmt.Printf("Stringer: %s\n", v.String())
default:
fmt.Printf("Other type: %T\n", v)
}
}
func main() {
PrintDetails("hello") // String: hello (length: 5)
PrintDetails(42) // Integer: 42
}Generics vs Interfaces
When to use generics vs interfaces:
- Use generics when you need type safety and performance
- Use interfaces when you need polymorphism at runtime
// Interface approach
type Printer interface {
Print()
}
type StringPrinter struct{ value string }
func (s StringPrinter) Print() { fmt.Println(s.value) }
type IntPrinter struct{ value int }
func (i IntPrinter) Print() { fmt.Println(i.value) }
// Generic approach
func Print[T any](value T) {
fmt.Println(value)
}Best Practices
- Use meaningful type parameter names:
Tfor general types,KandVfor key-value pairs - Keep constraints simple: Use the most specific constraint you can
- Avoid overusing generics: Sometimes interfaces or concrete types are simpler
- Consider performance: Generics have minimal runtime overhead
- Document your generics: Explain what types are expected
Common Patterns
Generic Map Function
func Map[T, U any](slice []T, mapper func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = mapper(v)
}
return result
}Generic Filter Function
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}Generic Set
type Set[T comparable] map[T]bool
func NewSet[T comparable]() Set[T] {
return make(Set[T])
}
func (s Set[T]) Add(item T) {
s[item] = true
}
func (s Set[T]) Contains(item T) bool {
return s[item]
}Generics are a powerful addition to Go that can make your code more reusable and type-safe. Start with simple cases and gradually use more advanced features as you become comfortable.
For more information, check the Go generics proposal and the generics tutorial.
If you’re new to Go interfaces, you might want to read about Go interfaces first.