Introduction: What are Generics and why they appeared in Go
For a long time, the Go community managed without generics (parameterized types). Developers used interface{} and manual type casting, which led to cumbersome code and runtime errors. With the release of version Go 1.18 (February 2022), generics — one of the most anticipated features — were officially added to the language.
Generics (or parametric polymorphism) allow you to write functions and data structures that work with any types while maintaining strict typing at compile time. This means you can write one algorithm (e.g., sorting or searching) and use it for int, string, custom structs — without code duplication and without losing type safety.
In this article, we will explore why generics are needed in Go, how to use them correctly, and what pitfalls to consider.
Main part: How Generics work in Go
1. Type parameter syntax
The basic syntax of generics in Go looks like this: after the function or type name, type parameters are specified in square brackets. Let's look at a simple example — a function that returns the passed value (an identity analog):
package main
import "fmt"
// Generic function: T is a type parameterfunc Print[T any](value T) { fmt.Println(value)}
func main() { Print[int](42) // explicit type specification Print[string]("Go") // explicit type specification Print("Go 1.18") // implicit type inference}The keyword any is an alias for interface{}. It means the function accepts a value of any type. However, the true power of generics is revealed when using constraints.
2. Constraints and interfaces
Constraints allow you to specify which operations can be performed on the type parameter. For example, to add two numbers, you need to guarantee that the type supports operators +, -, *, etc. In Go, this is done using special interfaces from the golang.org/x/exp/constraints package or custom interfaces:
package main
import ( "fmt" "golang.org/x/exp/constraints")
// Constraint: T must support comparisonfunc Max[T constraints.Ordered](a, b T) T { if a > b { return a } return b}
func main() { fmt.Println(Max(10, 20)) // 20 fmt.Println(Max("apple", "banana")) // banana // fmt.Println(Max([]int{1}, []int{2})) // Compilation error: slices are not ordered}The built-in interface constraints.Ordered includes types that support operators <, <=, >, >=: all numeric types and string.
You can also create custom constraints:
// Custom constraint: the type must have a String() methodtype Stringer interface { String() string}
func Describe[T Stringer](v T) string { return "Value: " + v.String()}3. Generic structs and methods
Generics in Go work not only with functions but also with custom types. Let's look at a classic example — a generic stack:
package main
import "fmt"
// Generic struct Stacktype Stack[T any] struct { items []T}
// Push adds an element to the stackfunc (s *Stack[T]) Push(item T) { s.items = append(s.items, item)}
// Pop removes an element from the stackfunc (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}
func main()