Введение: что такое Generics и почему они появились в Go?
Долгое время язык Go (Golang) оставался одним из немногих современных языков без поддержки обобщённого программирования (generics). Разработчикам приходилось либо дублировать код для разных типов данных, либо использовать интерфейсы и пустые interface{}, что приводило к потере типобезопасности и необходимости выполнять приведение типов в рантайме.
С выходом версии Go 1.18 (февраль 2022 года) в языке появились долгожданные дженерики. Generics — это механизм, позволяющий писать функции, типы и структуры данных, которые работают с произвольными типами, сохраняя при этом строгую типизацию на этапе компиляции.
В этой статье мы разберём:
- Зачем нужны дженерики в Go;
- Как объявлять обобщённые функции и типы;
- Что такое constraints и как их использовать;
- Практические примеры применения.
Зачем нужны Generics в Go?
До появления дженериков типичная проблема выглядела так: нужно написать функцию, которая работает со срезами разных типов (например, int, string, float64).
Без дженериков приходилось либо писать три отдельные функции:
func SumInts(nums []int) int {
total := 0
for _, v := range nums {
total += v
}
return total
}
func SumFloats(nums []float64) float64 {
total := 0.0
for _, v := range nums {
total += v
}
return total
}
Либо использовать interface{} с приведением типов — небезопасно и неудобно.
С дженериками достаточно одной функции:
func Sum[T int | float64](nums []T) T {
var total T
for _, v := range nums {
total += v
}
return total
}
Преимущества очевидны:
- Типобезопасность — ошибки типов отлавливаются на этапе компиляции;
- Уменьшение дублирования кода — одна функция для многих типов;
- Читаемость — код становится более лаконичным и понятным.
Основы синтаксиса Generics в Go
Объявление обобщённой функции
Базовый синтаксис использует квадратные скобки [] для указания параметров типа:
func ИмяФункции[ИмяТипа constraint](параметры) результат {
// тело функции
}
Пример простой функции, которая возвращает переданное значение:
func Identity[T any](value T) T {
return value
}
func main() {
fmt.Println(Identity[int](42)) // 42
fmt.Println(Identity[string]("Go")) // Go
}
Go может автоматически выводить тип, поэтому явное указание [int] необязательно:
fmt.Println(Identity(42)) // Go сам поймёт, что T = int
fmt.Println(Identity("Go")) // T = string
Обобщённые типы (структуры)
Дженерики работают не только с функциями, но и с пользовательскими типами:
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() {
intStack := Stack[int]{}
intStack.Push(10)
intStack.Push(20)
val, ok := intStack.Pop()
fmt.Println(val, ok) // 20 true
}
Constraints: ограничения для типов
В примерах выше мы использовали any — это встроенный constraint, означающий "любой тип". Однако часто требуется наложить ограничения: например, чтобы тип поддерживал сравнение или арифметические операции.
Встроенные constraints
any— синонимinterface{}, любой тип;comparable— типы, поддерживающие операторы==и!=(все сравнимые типы: числа, строки, указатели, каналы, структуры из сравнимых полей).
Пример с comparable — функция поиска элемента в срезе:
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]string{"a", "b", "c"}, "b")) // true
fmt.Println(Contains([]int{1, 2, 3}, 4)) // false
}
Пользовательские constraints
Вы можете объединять типы в собственные ограничения. Это делается через интерфейс с перечислением типов (type union):
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Min[T Number](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(10, 20)) // 10
fmt.Println(Min(3.14, 2.71)) // 2.71
}
Constraint с методами
Можно комбинировать type union с методами:
type Stringer interface {
~string | ~[]byte
String() string
}
Символ ~ означает, что constraint включает не только сам тип, но и любые типы, основанные на нём (например, type MyString string).
Практические примеры использования Generics
1. Универсальная сортировка
func SortSlice[T constraints.Ordered](slice []T) {
sort.Slice(slice, func(i, j int) bool {
return slice[i] < slice[j]
})
}
Здесь constraints.Ordered — пакет golang