Generics in Go (Go 1.18 and Later)

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.

Leveraging Generics

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.

Basic Syntax

Generics are defined using type parameters, which are specified in square brackets ([]) following the function or type name.

Generic Function Example

go
package 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.

Generic Types

You can also define generic types, such as data structures that can hold any type of value.

Generic Type Example: Stack

go
package 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.

Constraints

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.

Constraint Example: Numeric Operations

go
package 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).

Practical Examples

Generic Data Structure: Map

Implementing a generic map data structure that can handle any key-value types.

go
package 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 } }

Generic Algorithm: Filter

Implementing a generic filter function that filters a slice based on a predicate.

go
package 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] }

Conclusion

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.

Becoming a Senior Go Developer: Mastering Go and Its Ecosystem