Generics в Go: почему их ждали, а массово не используют?
Сообщество получило то, о чём просило, и ... проигнорировало. Как же так — дженерики в Go ломают привычные паттерны разработки?
554 открытий3К показов
Создателей Go 13 лет атаковали просьбами добавить дженерики. Мечта сбылась: новая функция должна была избавить от дублирования кода, упростить работу с алгоритмами и сделать типы более безопасными.
Заметили, что массового внедрения не произошло? Разработчики не спешат переписывать рабочий код на дженериках.
По каким причинам обобщённые типы оказались невостребованными? — ответим на этот вопрос вместе с участниками тг-канала Golang GO.
Почему дженерики не вытеснили интерфейсы?
Интерфейсы задают структуру программы: реализуют паттерны декоратора, роутера, зависимости. Дженерики лишь усиливают существующие интерфейсы, делают их безопаснее.
Разработчики, ожидавшие революцию в проектировании, обнаружили, что дженерики годятся только для узкого пула задач.
Тащить их везде может быть чревато не только потерей производительности, но и рефакторингом больших объёмов кода. Далеко не каждая компания сейчас потянет такие затраты. Тем более, если неясны перспективы.
Наиболее оправданный случай использования дженериков — замена interface{} в местах, где тип известен на этапе компиляции:
Когда всё-таки использовать дженерики?
Практика показывает, что обобщённые типы нашли применение в конкретных областях.
У нас они часто используются: много дженерик-функций для кеширования, маппингов, сохранения в базы данных, передачи по gRPC, работы с брокерами, почти вся CRUD-админка на дженериках. Из плюсов — меньше кода, из минусов — иногда сложно понять, что происходит с конкретной сущностью
1. Пулы соединений и кэширование
Наиболее очевидное применение дженериков — создание типизированных контейнеров.
Например, этот пул соединений будет работать с любым типом ресурсов, поддерживающих закрытие:
Аналогично с кэшированием. Можно создавать строго типизированный кэш, который возвращает значения правильного типа на этапе компиляции:
2. Математические операции
Дженерики избавляют от дублирования кода для разных числовых типов:
3. Функции высшего порядка
С появлением дженериков появилась возможность создавать типобезопасные функции высшего порядка для работы с коллекциями:
Я не представляю, как можно обойтись без дженериков, если идёт речь про обработку больших массивов разнообразных данных, где нужны собственные структуры и алгоритмы.
4. Обёртки для API и протоколов
Так выглядит обёртка ответа с типизированным полем результата на дженериках:
Компилятор гарантирует соответствие типов на этапе компиляции. До дженериков приходилось использовать json.RawMessage и распаковывать на уровне роутинга.
5. Репозитории и CRUD-операции
Типизированные интерфейсы для работы с данными:
Как дженерики повлияли на тесты?
Однозначно стало проще создавать универсальных хелперов для тестирования:
Почему дженерики в Go не такие, как в других языках?
Отсутствие обобщённых типов болезненно воспринимали разработчики, перешедшие в Go из Java, C#, C++ и других языков с дженериками.
Добавление функционального программирования — это тренд Rust, Kotlin и других современных языков. Без дженериков в Go было бы сложно переписывать код в функциональной парадигме с других языков.
Реализация фичи подвела: гошные методы не имеют собственные параметры типа. Методы наследуют дженерик-параметры самой структуры, и нельзя добавить новые.
Например, это работает:
А такая функция уже не скомпилируется:
Привычные паттерны из Rust, Java или C# в Go просто не реализуемы. Вместо методов приходится использовать функции:
Как дженерики изменили разработку на Go?
Типовые задачи в Го до появления дженериков решали иначе. Приходилось использовать пустой interface{}, генерировать типобезопасный код с помощью go:generate, stringer.
Несмотря на недостатки, дженерики поменяли подход к разработке.
Если у вас всё строится на дженериках, то можно считать, что нужен совершенно другой инструментарий и образ мышления для работы с таким кодом.
Обобщённые типы активно используются в библиотеках. Взять тот же samber/lo и функции Map, Filter, Reduce на дженериках. Ещё стандартная библиотека пополнилась пакетами slices и maps.
Что касается производительности — ожидания не оправдались. Думали, что избавились от преобразований типов и получили прирост скорости? На деле разница минимальна. В реальных приложениях узкое место не в типах, а в работе БД, сетевых запросах или бизнес-логике.
Что в итоге?
- Интерфейсы остаются основой архитектуры Go-приложений, дженерики их дополняют, но не заменяют.
- Обобщённые типы полезны только для конкретных задач. Применяйте по необходимости, когда дженерики устраняют дублирование кода или повышают типобезопасность.
- Упрощайте тестирование — универсальные хелперы для проверок и утверждений.
- Избегайте преждевременного обобщения — не пишите код через дженерики «на всякий случай».
- Параметры типов лучше всего подходят для контейнеров, кэшей, математических функций.
- Привычные паттерны из других языков не реализуемы — приходится адаптировать подходы под Go.
- Усложнение понимания кода — иногда непросто разобраться, что происходит с конкретной сущностью.
Поделитесь опытом использования дженериков — где они реально помогли, а где оказались излишними? Удивите примерами удачного и неудачного применения!
554 открытий3К показов




