Generics в Go: зачем нужны и как использовать
With the release of Go version 1.18, one of the most anticipated features appeared in the language — generics (generic programming). Until this moment, developers had to either duplicate code for different data types or use the empty interface interface{} with subsequent type casting. Generics solve this problem radically. In this article, we will look at why they are needed, how to use them in practice, and what pitfalls await you.
What are generics and why are they needed in Go?
Generics (or generic types) allow you to write functions, structures, and methods that work with any data types while maintaining strict typing. This means you can write one function that works with numbers, strings, and custom types without losing type information at compile time.
Before the advent of generics, Go developers used two main approaches:
- Code duplication — writing separate functions for
int,string,float64, etc. - Empty interface — using
interface{}with subsequent type assertion or reflection, which was unsafe and slow.
Generics solve these problems by allowing you to write universal yet type-safe code. The compiler checks types at compile time, eliminating runtime errors.
Generics syntax: type parameters
The main idea of generics in Go is type parameters. They are declared in square brackets after the function or type name.
Let's look at a simple example — a function that returns the minimum of two values:
package main
import "fmt"
// Generic function with type parameter Tfunc Min[T comparable](a, b T) T { if a < b { return a } return b}
func main() { fmt.Println(Min[int](10, 20)) // 10 fmt.Println(Min[string]("a", "b")) // "a" fmt.Println(Min(3.14, 2.71)) // 2.71 (type is inferred automatically)}Here T is the type parameter. comparable is a constraint that says the type T must support comparison operators. Without this constraint, the compiler would not allow using <.
Important: Go can automatically infer type parameters from arguments, so in most cases you don't need to specify them explicitly.
Constraints in Go generics
Constraints are a key element of generics. They define which operations are allowed for type parameters. Without constraints, the compiler doesn't know which methods or operators can be applied to T.
Go has several built-in constraints:
any— a synonym forinterface{}, allows any type.comparable— allows types that support==and!=.constraints.Ordered— from the packagegolang.org/x/exp/constraints, allows all ordered types (numbers and strings).
You can also create your own constraints using interfaces:
package main
import "fmt"
// Custom constraint: the type must have a String() methodtype Stringer interface { String() string}
// Function working with any Stringerfunc Print[T Stringer](value T) { fmt.Println(value.String())}
type User struct { Name string}
func (u User) String() string { return "User: " + u.Name}
func main() { user := User{Name: "Alice"} Print(user) // User: Alice}Note: in Go 1.18+, interfaces can be used as constraints, and they can contain not only methods but also concrete