Introduction: what are Generics and why did they appear in Go?
For a long time, the Go (Golang) community managed without generics (generic programming). Developers used interfaces, empty interface{}, and code generation to work around the limitations of strict typing. In Go version 1.18 (February 2022), generics were finally added to the language — a powerful tool that allows you to write functions and structures that work with different data types without losing type safety.
In this article, we will break down why Generics are needed in Go, how they work, and learn how to apply them in practice. The material is designed for beginner developers who are already familiar with the basics of Go: variables, functions, and slices.
Why are generics needed in Go?
Imagine you need to write a function that finds the minimum value in a slice of numbers. Without generics, you would have to write separate functions for int, float64, and other types:
// Without generics — three identical functionsfunc MinInt(a, b int) int { if a < b { return a } return b}
func MinFloat64(a, b float64) float64 { if a < b { return a } return b}This leads to code duplication. Previously, this problem was solved using interface{} and type casting, but this approach lost compile-time safety:
func Min(a, b interface{}) interface{} { // Comparison and casting — easy to make mistakes return a}Generics solve three key problems:
- Type safety — type errors are caught at compile time.
- Eliminating duplication — one function works with any types.
- Code readability — explicit indication of which types are supported.
Syntax basics: type parameters and constraints
Generics in Go are built on two concepts: type parameters and constraints.
Type parameters
Type parameters are specified in square brackets after the function or type name. By convention, the letter T (for Type) is most often used, but any names can be used:
// Function with one type parameter Tfunc Print[T any](value T) { fmt.Println(value)}
// UsagePrint[int](42) // explicitly specify the typePrint("Hello") // compiler infers the type automaticallyThe keyword any is an alias for the empty interface interface{}. It means the function accepts any type.
Constraints
Constraints define which types can be used with a generic. For example, to compare numbers, the type needs to support the < and > operators. Built-in constraints for numeric types are in the golang.org/x/exp/constraints package:
import "golang.org/x/exp/constraints"
// Constraint — only integersfunc Sum[T constraints.Integer](numbers []T) T { var total T for _, n := range numbers { total += n } return total}You can also create your own constraints using interfaces:
// Custom constraint: the type must have a String() methodtype Stringer interface { String() string}
func Describe[T Stringer](value T) string { return "Value: " + value.String()}Practical examples of using Generics
Let's look at several real-world scenarios where generics significantly simplify code.
1. Universal search function in a slice
// Finds the index of an element in a slice. -1 if not found.func Index[T comparable](slice []T, target T