With the release of Go 1.18, generics were introduced to the language, allowing developers to write type-safe and reusable code. This powerful feature enables the definition of functions, types, and data structures that can operate on any data type. This guide covers the basics of generics, syntax and patterns, and practical examples.
Generics allow you to write more flexible and reusable code without sacrificing type safety. They are particularly useful for implementing common data structures and algorithms that work with any type.
Generics are defined using type parameters, which are specified in square brackets ([]
) following the function or type name.
gopackage main
import "fmt"
// A generic function that swaps two values of any type.
func Swap[T any](a, b T) (T, T) {
return b, a
}
func main() {
x, y := Swap(1, 2)
fmt.Println(x, y) // Output: 2 1
s1, s2 := Swap("hello", "world")
fmt.Println(s1, s2) // Output: world hello
}
In this example, Swap
is a generic function that can operate on any type T
. The type parameter T
is specified in square brackets ([T any]
), where any
is a type constraint indicating that T
can be any type.
You can also define generic types, such as data structures that can hold any type of value.
gopackage main
import "fmt"
// Stack is a generic type that holds values of any type.
type Stack[T any] struct {
elements []T
}
func (s *Stack[T]) Push(value T) {
s.elements = append(s.elements, value)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
value := s.elements[len(s.elements)-1]
s.elements = s.elements[:len(s.elements)-1]
return value, true
}
func main() {
var intStack Stack[int]
intStack.Push(1)
intStack.Push(2)
value, ok := intStack.Pop()
if ok {
fmt.Println(value) // Output: 2
}
var stringStack Stack[string]
stringStack.Push("hello")
stringStack.Push("world")
valueStr, ok := stringStack.Pop()
if ok {
fmt.Println(valueStr) // Output: world
}
}
In this example, Stack
is a generic type with a type parameter T
. The Push
and Pop
methods operate on values of type T
.
Type constraints restrict the types that can be used with generics. The any
constraint allows any type, but you can specify more restrictive constraints using interfaces.
gopackage main
import "fmt"
// Numeric is a constraint that allows only numeric types.
type Numeric interface {
~int | ~float64
}
// Sum is a generic function that sums two numeric values.
func Sum[T Numeric](a, b T) T {
return a + b
}
func main() {
fmt.Println(Sum(1, 2)) // Output: 3
fmt.Println(Sum(1.5, 2.5)) // Output: 4
}
In this example, the Numeric
interface restricts the type parameter T
to only numeric types (e.g., int
and float64
).
Implementing a generic map data structure that can handle any key-value types.
gopackage main
import "fmt"
// GenericMap is a generic type for a map.
type GenericMap[K comparable, V any] struct {
data map[K]V
}
func NewGenericMap[K comparable, V any]() *GenericMap[K, V] {
return &GenericMap[K, V]{data: make(map[K]V)}
}
func (m *GenericMap[K, V]) Put(key K, value V) {
m.data[key] = value
}
func (m *GenericMap[K, V]) Get(key K) (V, bool) {
value, ok := m.data[key]
return value, ok
}
func main() {
intToStrMap := NewGenericMap[int, string]()
intToStrMap.Put(1, "one")
intToStrMap.Put(2, "two")
value, ok := intToStrMap.Get(1)
if ok {
fmt.Println(value) // Output: one
}
strToIntMap := NewGenericMap[string, int]()
strToIntMap.Put("one", 1)
strToIntMap.Put("two", 2)
valueInt, ok := strToIntMap.Get("two")
if ok {
fmt.Println(valueInt) // Output: 2
}
}
Implementing a generic filter function that filters a slice based on a predicate.
gopackage main
import "fmt"
// Filter is a generic function that filters elements of a slice based on a predicate.
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
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
isEven := func(n int) bool { return n%2 == 0 }
evenNumbers := Filter(numbers, isEven)
fmt.Println(evenNumbers) // Output: [2 4]
words := []string{"apple", "banana", "cherry"}
startsWithB := func(s string) bool { return s[0] == 'b' }
bWords := Filter(words, startsWithB)
fmt.Println(bWords) // Output: [banana]
}
Generics in Go enhance the language by allowing the definition of type-safe, reusable functions and data structures. Understanding the syntax and patterns of generics, along with practical examples, enables developers to write more flexible and maintainable code. While generics introduce new capabilities, it's important to use them judiciously and be mindful of their impact on code readability and performance.