Go долго жил без дженериков. Если нужно было написать одну и ту же функцию для
int и float64, приходилось копировать код или использовать interface{} с приведением типов. В версии 1.18 это наконец исправили.Разберём, что такое дженерики в Go, как они работают и зачем нужны type constraints.
Зачем нужны дженерики
Представьте, что вам нужна функция поиска минимума. Без дженериков пришлось бы писать отдельную версию для каждого типа:
func MinInt(a, b int) int {
if a < b { return a }
return b
}
func MinFloat(a, b float64) float64 {
if a < b { return a }
return b
}С дженериками всё сводится к одной функции:
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}Теперь
Min работает с любым типом, который поддерживает сравнение. Вызывается так же просто, как обычная функция. Go сам определяет тип по аргументам:
fmt.Println(Min(3, 5)) // 3
fmt.Println(Min(3.14, 2.71)) // 2.71
fmt.Println(Min("a", "b")) // "a"
Синтаксис
Типовой параметр указывается в квадратных скобках перед обычными аргументами.
После имени параметра идёт constraint, которое определяет, какие типы допустимы:
func Print[T any](value T) {
fmt.Println(value)
}Здесь
T any означает, что T может быть чем угодно. Можно использовать несколько типовых параметров:
func Pair[T, U any](first T, second U) (T, U) {
return first, second
}Встроенные ограничения
Go предлагает несколько готовых constraint'ов.
any принимает любой тип. Подходит, когда вам не нужны никакие операции над значением, кроме передачи и хранения.comparable принимает типы, которые можно сравнивать через == и !=. Полезно для поиска по слайсу или использования в качестве ключа map.func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}cmp.Ordered из пакета cmp (Go 1.21+) принимает типы с поддержкой операторов <, >, <=, >=. Это все числовые типы и строки.func Max[T cmp.Ordered](values ...T) T {
max := values[0]
for _, v := range values[1:] {
if v > max {
max = v
}
}
return max
}Свои ограничения
Можно создавать собственные constraint'ы через интерфейсы с перечислением типов:
type Number interface {
int | int8 | int16 | int32 | int64 |
float32 | float64
}
func Sum[T Number](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}Оператор
~ позволяет принимать не только базовые типы, но и все типы, основанные на них. Если у вас есть
type MyInt int, то constraint ~int примет и MyInt:type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type MyInt int
func Double[T Integer](v T) T {
return v * 2
}
var x MyInt = 5
Double(x) // работает, потому что MyInt основан на intConstraint'ы можно комбинировать. Если нужен тип, который одновременно поддерживает сравнение и имеет метод
String(), это тоже реализуемо:type OrderedStringer interface {
cmp.Ordered
fmt.Stringer
}Дженерики убирают дублирование кода без потери типобезопасности. В Go они реализованы лаконично, без сложной иерархии типов.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoDeep