Introduction: what are Generics and why did they appear in Go?
\nFor a long time, the Go language (Golang) remained one of the few modern languages without support for generic programming (generics). Developers had to either duplicate code for different data types or use interfaces and empty interface{}, which led to a loss of type safety and the need to perform type casting at runtime.
With the release of Go version 1.18 (February 2022), the long-awaited generics appeared in the language. Generics is a mechanism that allows you to write functions, types, and data structures that work with arbitrary types while maintaining strict typing at compile time.
\nIn this article, we will cover:
\n- \n
- Why generics are needed in Go; \n
- How to declare generic functions and types; \n
- What constraints are and how to use them; \n
- Practical usage examples. \n
Why are Generics needed in Go?
\nBefore the advent of generics, a typical problem looked like this: you need to write a function that works with slices of different types (e.g., int, string, float64).
Without generics you had to either write three separate functions:
\nfunc SumInts(nums []int) int {\n total := 0\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nfunc SumFloats(nums []float64) float64 {\n total := 0.0\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nOr use interface{} with type casting — unsafe and inconvenient.
With generics one function is enough:
\nfunc Sum[T int | float64](nums []T) T {\n var total T\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nThe advantages are obvious:
\n- \n
- Type safety — type errors are caught at compile time; \n
- Reduced code duplication — one function for many types; \n
- Readability — the code becomes more concise and understandable. \n
Basics of Generics syntax in Go
\nDeclaring a generic function
\nThe basic syntax uses square brackets [] to specify type parameters:
func FunctionName[TypeName constraint](parameters) result {\n // function body\n}\n\nExample of a simple function that returns the passed value:
\nfunc Identity[T any](value T) T {\n return value\n}\n\nfunc main() {\n fmt.Println(Identity[int](42)) // 42\n fmt.Println(Identity[string]("Go")) // Go\n}\n\nGo can automatically infer the type, so explicitly specifying [int] is optional:
fmt.Println(Identity(42)) // Go will understand that T = int\nfmt.Println(Identity("Go")) // T = string\n\n\nGeneric types (structs)
\nGenerics work not only with functions but also with custom types:
\ntype Stack[T any] struct {\n elements []T\n}\n\nfunc (s *Stack[T]) Push(value T) {\n s.elements = append(s.elements, value)\n}\n\nfunc (s *Stack[T]) Pop() (T, bool) {\n if len(s.elements) == 0 {\n var zero T\n return zero, false\n }\n value := s.elements[len(s.elements)-1]\n s.elements = s.elements[:len(s.elements)-1]\n return value, true\n}\n\nfunc main() {\n intStack := Stack[int]{}\n intStack.Push(10)\n intStack.Push(20)\n val, ok := intStack.Pop()\n fmt.Println(val, ok) // 20 true\n}\n\n\nConstraints: type restrictions
\nIn the examples above, we used any — a built-in constraint meaning "any type". However, it is often necessary to impose restrictions: for example, that the type supports comparison or arithmetic operations.