Всё началось с Go 1.18: почему дженерики — это не просто модная фича?
Долгое время сообщество Go гордилось простотой языка. Отсутствие обобщённого программирования (generics) считалось осознанным выбором: «нам не нужны сложные шаблоны, мы пишем понятный код». Но реальность взяла своё. Разработчики устали копипастить функции для int, string и float64 — или использовать interface{} с последующим приведением типов, теряя безопасность на этапе компиляции.
Выход Go 1.18 (2022 год) стал революцией. Дженерики (generics) добавили в язык параметры типов (type parameters) и ограничения (constraints). Теперь Go-программисты могут писать обобщённые структуры данных и алгоритмы, не жертвуя производительностью и строгой типизацией.
В этой статье на pythonlib.ru мы разберёмся, как работают generics в Go, увидим реальные примеры кода и поймём, когда их стоит использовать, а когда — нет. Поехали!
Что такое generics в Go простыми словами?
Представьте, что вы пишете функцию, которая находит минимальный элемент в срезе. Без дженериков вам пришлось бы создавать три версии: для int, float64 и string. Или использовать interface{} с приведением типов — медленно и небезопасно.
Generics позволяют написать одну функцию, которая работает с любым типом, удовлетворяющим определённым условиям. Синтаксис простой: после имени функции в квадратных скобках указываются параметры типов.
Синтаксис: квадратные скобки и слово any
package main
import "fmt"
// Min — обобщённая функция для поиска минимума
func Min[T any](a, b T) T {
// Ошибка: оператор < не определён для any
// if a < b {
// return a
// }
return b
}
Код выше не скомпилируется, потому что any — это пустой интерфейс (синоним interface{}). Компилятор не знает, поддерживает ли тип оператор <. Нужно добавить ограничение (constraint).
Ограничения типов: constraints
В Go ограничения задаются через интерфейсы. Можно использовать встроенные из пакета golang.org/x/exp/constraints или написать свои.
import (
"fmt"
"golang.org/x/exp/constraints"
)
// Min работает с любым упорядоченным типом
func Min[T constraints.Ordered](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
fmt.Println(Min("apple", "banana")) // "apple"
}
Обратите внимание: constraints.Ordered включает все типы, для которых определены операторы <, <=, >, >= — это int, float, string.
Практические примеры: от простого к сложному
Давайте посмотрим, как generics в Go упрощают написание реального кода.
1. Обобщённый стек (Stack)
Без дженериков пришлось бы хранить []interface{} и делать type assertion. С generics — элегантно и безопасно.
package main
import "fmt"
// Stack — обобщённая структура данных
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func main() {
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, ok := intStack.Pop()
fmt.Println(val, ok) // 2 true
strStack := Stack[string]{}
strStack.Push("hello")
strStack.Push("world")
fmt.Println(strStack.Pop()) // world true
}
2. Обобщённая функция для фильтрации среза
Очень полезно для обработки данных: один раз написали, используем везде.
// Filter возвращает новый срез, содержащий только элементы,
// для которых предикат вернул true
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
evens := Filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4 6]
words := []string{"cat", "dog", "elephant", "ant"}
longWords := Filter(words, func(s string) bool {
return len(s) > 3
})
fmt.Println(longWords) // [elephant]
}
3. Дженерики с несколькими параметрами типов
Можно объявлять два и более параметра. Например, функция для создания пары.
func MakePair[K comparable, V any](key K, value V) Pair[K, V] {
return Pair[K, V]{Key: key, Value: value}
}
type Pair[K comparable, V any] struct {
Key K
Value V
}
func main() {
p := MakePair("id", 42)
fmt.Println(p.Key, p.Value) // id 42
}
Обратите внимание: comparable — это встроенное ограничение, которое разрешает сравнение с помощью == и !=. Идеально для ключей map.
Ограничения: что можно и что нельзя?
Дженерики в Go — мощный инструмент, но у них есть свои границы. Важно понимать, чтобы не попасть в ловушку.
- Нельзя использовать операторы (кроме сравнения через
==и!=для comparable). Для арифметики нужны custom constraints. - Нельзя использовать дженерики с методами — параметр типа можно задать только для типа или функции, но не для отдельного метода.
- Производительность: дженерики Go компилируются в мономорфный код (как в C++), поэтому накладных расходов на runtime нет. Но увеличивается размер бинарника.
- Читаемость: если в коде слишком много
[T any, U comparable], код может стать сложным. Используйте дженерики осмысленно.