Обобщения или generics появились в Go начиная с версии 1.18. Долгое время Go считался консервативным языком, где ради простоты были исключены generics, но с ростом числа проектов и развитием экосистемы возникла потребность в написании универсальных функций и структур. Это особенно критично для контейнерных структур, алгоримов обработки коллекций, и инфраструктурного кода.
Проблема: до появления generics приходилось дублировать код или использовать пустые интерфейсы (interface{}), что приводило к потере типобезопасности и снижению производительности, а также усложнению отладки.
Решение: generics реализованы в Go с помощью параметров типа, которые указываются в квадратных скобках у функций и типов. С помощью constraints возможно ограничивать допустимые параметры типов. Это позволяет писать обобщённые функции без потери типобезопасности.
Пример кода:
package main import "fmt" type Adder[T any] func(a, b T) T func Sum[T any](slice []T, add Adder[T]) T { var result T for _, v := range slice { result = add(result, v) } return result } func intAdder(a, b int) int { return a + b } func main() { nums := []int{1, 2, 3, 4} sum := Sum(nums, intAdder) fmt.Println(sum) }
Ключевые особенности:
Можно ли использовать арифметические операции (+, -, *, /) для любых параметров типа T в generics?
Нет. Компилятор Go не знает, поддерживает ли тип параметра арифметику. Для этого нужно указывать constraint, например, интерфейс с operator constraints начиная с Go 1.18+.
Пример кода:
type Addable interface { int | float64 | uint } func Sum[T Addable](slice []T) T { var result T for _, v := range slice { result += v } return result }
Могут ли generic-контейнеры содержать методы с разным поведением для разных типов?
Нет. Методы generic-структур или функций одинаковы для всех типов, если не использовать type switch внутри метода. Поведение должно быть определено через constraints или чисто параметризовано.
Можно ли создавать типы с параметрами типа (generic types) на уровне пакета, а не только функций?
Да, начиная с Go 1.18, можно создавать как generic функции, так и generic структурные типы:
type Stack[T any] struct { items []T } func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
Внутри библиотеки для работы с коллекциями был реализован универсальный Map-функционал через interface{}:
Плюсы:
Минусы:
В том же проекте перешли на generics и задали ограничения через интерфейсы:
Плюсы:
Минусы: