Работа мечты в один клик 💼

💭Мечтаешь работать в Сбере, но не хочешь проходить десять кругов HR-собеседований? Теперь это проще, чем когда-либо!
💡AI-интервью за 15 минут – и ты уже на шаг ближе к своей новой работе.
Как получить оффер?
📌 Зарегистрируйся
📌 Пройди AI-интервью
📌 Получи обратную связь сразу же!
HR больше не тянут время – рекрутеры свяжутся с тобой в течение двух дней! 🚀
Реклама. ПАО СБЕРБАНК, ИНН 7707083893. Erid 2VtzquscAwp
Обобщенное программирование
Обобщенное программирование (ОП) представляет собой парадигму разработки программного обеспечения, которая позволяет писать гибкий и универсальный код, способный работать с различными типами данных.
До версии 1.18 в Go было несколько способов реализации обобщенного программирования:
- С помощью интерфейсов, конструкций switch-case и приведения типов.
- При помощи пакета reflect, который реализует механизм runtime-рефлексии, позволяя взаимодействовать с объектами произвольных типов. Рефлексия – это способность программы исследовать собственную структуру, в частности, через типы.
- Посредством механизма кодогенерации.
На протяжении долгих лет разработчикам приходилось самим реализовывать функционал ОП в Go. Только в версии 1.18, выпущенной в марте 2022 года, была добавлена поддержка дженериков, реализующих механизмы ОП, что стало самым значимым нововведением с момента выпуска языка. Дженерики, как и представленные выше подходы, имеют свои преимущества и недостатки и до сих пор являются темой споров в сообществе разработчиков.
Дженерики
Основополагающей концепцией ОП являются дженерики – это способ написания кода, позволяющий различным сущностям программы не зависеть от конкретных типов.
Давайте изучим поведение и основные составляющие дженериков на примере конкретной задачи. Допустим, при работе над проектом нас попросили реализовать функцию суммирования целочисленных значений мапы с ключами типа string
. После освоения всех предыдущих частей самоучителя это задание не должно вызвать особого затруднения:
func SumMapValues(mp map[string]int) int {
var sum int
for _, val := range mp {
sum += val
}
return sum
}
Спустя некоторое время функционал приложения расширился, и появилась необходимость сделать такую же функцию для суммирования значений типа float64 мапы с целочисленными ключами. Не беда, нужно всего лишь скопировать предыдущий вариант SumMapValues
, поменять типы данных, и все готово. Но в дальнейшем может понадобиться, чтобы функция SumMapValues
могла работать со значениями и ключами произвольных типов, поэтому применяемый нами подход приведет лишь к дублированию кода и возможным ошибкам. Что также немаловажно, он нарушает принцип разработки ПО под названием DRY (don`t repeat yourself – не повторяй себя).
Самое время переписать функцию с использованием дженериков, чтобы она могла не зависеть от конкретных типов:
func SumMapValues[K comparable, V int64 | float64](mp map[K]V) V {
var sum V
for _, val := range mp {
sum += val
}
return sum
}
Можно заметить несколько отличий обобщенной функции от обычной. Самое явное заключается в указании в квадратных скобках специальных значений, которые называются «типизированные параметры» или «типы как параметры» (type parameters). Они позволяют передавать в функцию тип в качестве параметра. В нашем примере type parameters представлены символами K
и V
и изменяются в пределах ограничений вида comparable
и int64 | float64
соответственно, которые называются type constraint.
Ключевое слово comparable – это предопределенный интерфейс для описания типов данных, поддерживающих сравнение с помощью операторов ==
и !=
. Примерами comparable типов являются bool, int, float, string и другие. Comparable типами не являются слайсы, функции, мапы и некоторые другие с определенными условиями.
В версии 1.18 помимо comparable был также добавлен интерфейс any – псевдоним для interface{}
, который может представлять любой тип.
Type constraint
В Go type constraint должен представлять из себя интерфейс, который определяет набор типов (type set), а именно типы, реализующие методы этого интерфейса.
Чтобы в полной мере понять type constraints, нужно расширить наше представление об интерфейсах. До введения поддержки ОП в спецификации Go содержалось уже известное нам правило: тип, реализующий все методы интерфейса, удовлетворяет ему. Теперь представьте, что вместо методов указываются типы данных, и принято следующее соглашение: тип, входящий в набор типов интерфейса, реализует этот интерфейс. Иными словами, произвольный тип T
удовлетворяет интерфейсу Inter при выполнении хотя бы одного из условий:
Type set T
является подмножествомtype set Inter
, при этомT
является интерфейсом.T
содержится вtype set Inter
, при этомT
не является интерфейсом.
Стоит запомнить, что в общем случае элемент интерфейса может быть представлен тремя способами: произвольным типом T
, базовым типом ~T
и объединением типов вида type1 | type2 | … | typeN
. Рассмотрим каждое из этих обозначений подробнее:
- Назначение произвольного типа T понятно из его названия, он может заменять любой тип. С ним мы сталкивались при реализации функции SumMapValues.
- Базовый тип ~T содержит токен тильда ("~"), добавленный в версии 1.18, и обозначает набор типов, базовым для которых является T. К примеру, в коде ниже тип
int
является базовым для типаImplementSome
, который, в свою очередь, реализует интерфейсSomeInterface
:
// Интерфейс для обозначения всех типов с базовым типом int, реализующих метод SomeMethod()
type SomeInterface interface {
~int
SomeMethod()
}
type ImplementSome int
func (is ImplementSome) SomeMethod() {
// реализация метода
}
- Объединение типов определяет совокупность всех типов, которые могут использоваться данным интерфейсом. К примеру, мы можем создать интерфейс
Values
с объединением типов int64 и float64, который выступит в качествеtype constraint
значений мапы и заменит записьint64 | float64
:
type Values interface {
int64 | float64
}
func SumMapValues[K comparable, V Values](mp map[K]V) V {}
Отметим, что формы записи объектов, подобные представленным ниже, недопустимы:
type CustomFloat float64
type InvalidInterface interface {
T // ошибка: T - type parameter
string | ~T // ошибка: ~T - type parameter
~float64 | CustomFloat // ошибка: ~float64 включает CustomFloat
~error // ошибка: error является интерфейсом
}
var FloatVar Float // ошибка
var comparableVar comparable // ошибка
type FloatType Float // ошибка
type FloatStruct struct {
flt Float // ошибка
}
Представленный интерфейс Float
является частью пакета constraints, в котором разработчики языка собрали часто используемые ограничения. Приведем некоторые из них:
// // Float допускает любой вещественный тип
type Float interface {
~float32 | ~float64
}
// Integer допускает любой целочисленный тип
type Integer interface {
Signed | Unsigned
}
// Ordered допускает любой тип, поддерживающий операторы сравнения (<, <, <=, >=)
type Ordered interface {
Integer | Float | ~string
}
Инстанцирование и type inference
Продолжим рассмотрение механизмов ОП на примере функции SumMapValues
. Вызовем её для двух различных мап, явно указав в квадратных скобках используемый тип данных:
func main() {
mapFloat := map[string]float64{
"1": 1.5,
"2": 2.5,
}
mapInt := map[string]int64{
"1": 10,
"2": 20,
}
sumMapFloat := SumMapValues[string, float64](mapFloat)
sumMapInt := SumMapValues[string, int64](mapInt)
fmt.Println("mapFloat:", sumMapFloat) // mapFloat: 4
fmt.Println("mapInt:", sumMapInt) // mapInt: 30
}
Указание «типа в качестве аргумента» (type argument), как в примере выше [string, float64]
, принято называть инстанцированием. При этом процессе компилятор заменяет все аргументы типов на требуемые параметры и проверяет, что каждый тип соответствует его type constraint.
Стоит отметить важное нововведение в версии 1.18: при инстанцировании нет необходимости явно указывать типы передаваемых аргументов. Автоматическое сопоставление типов аргументов с типами параметров производится компилятором и носит название «выведение типа аргумента функции» (type inference). Применительно к нашему коду, этот механизм позволяет не указывать типы [string, float64]
и [string, int64]
при вызовах функции SumMapValues
:
sumMapFloat := SumMapValues(mapFloat)
sumMapInt := SumMapValues(mapInt)
Type inference
распространяется только на type parameters
, указанные в параметрах функции, но не по отдельности в её теле или возвращаемых значениях.
Дженерики в стандартной библиотеке Go
В версии 1.18 стандартная библиотека была расширена тремя экспериментальными пакетами, основанными на дженериках: slices для работы со слайсами, maps для взаимодействия с мапами и constraints для задания распространенных ограничений.
Давайте посмотрим на реализации некоторых функций из пакетов slices и maps, чтобы на примерах увидеть рассмотренные ранее концепции:
slices.Equal
сравнивает два слайса по длине и значениям:
func Equal[S ~[]E, E comparable](s1, s2 S) bool {
if len(s1) != len(s2) {
return false
}
for i := range s1 {
if s1[i] != s2[i] {
return false
}
}
return true
}
maps.Equal
проверяет, что две мапы содержат одинаковые пары ключ/значение:
func Equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool {
if len(m1) != len(m2) {
return false
}
for k, v1 := range m1 {
if v2, ok := m2[k]; !ok || v1 != v2 {
return false
}
}
return true
}
Задачи
Пришло время закрепить изученную теорию на практических задачах. Отметим, что две последние являются довольно объемными, зато их выполнение позволит хорошо разобраться в дженериках. В случае возникновения трудностей вы всегда можете заглянуть в решение и посмотреть вариант реализации функций.
Фильтрация слайса
Напишите дженерик-функцию Filter
для отбора элементов слайса по определенному признаку. В качестве параметров она принимает слайс произвольного типа и функцию-предикат predicate с параметром произвольного типа и возвращаемым значением типа bool
. Если элемент удовлетворяет заданному в предикате условию, то predicate
возвращает true
, иначе – false
. Функция Filter
возвращает слайс с отобранными элементами.
Пример вызова функции Filter:
slc := []int{1, 2, 3, 4}
predicate := func(val int) bool {
return val%2 == 0
}
fmt.Println(Filter(slc, predicate)) // [2 4]
Решение
func Filter[T any](slc []T, predicate func(T) bool) []T {
var result []T
for _, value := range slc {
if predicate(value) {
result = append(result, value)
}
}
return result
}
Удаление повторов
Напишите дженерик-функцию RemoveDuplicates
для удаления повторов из слайса comparable
типа. Она принимает в качестве параметра слайс и возвращает новый слайс без повторяющихся значений.
Пример вызова функции RemoveDuplicates:
func main() {
slc := []int{1, 2, 1, 3, 2, 9, 5}
fmt.Println(RemoveDuplicates(slc)) // 1 2 3 9 5
}
Решение
func RemoveDuplicates[T comparable](slc []T) []T {
var result []T
checked := make(map[T]bool)
for _, val := range slc {
if _, ok := checked[val]; !ok {
checked[val] = true
result = append(result, val)
}
}
return result
}
Обобщенный кэш
Реализуйте с помощью дженериков механизм кэширования данных. Он состоит из нескольких компонентов:
- Структура
Cache
, содержащая мапу с ключами типа string и значениями произвольного типаT
, которая будет выступать в качестве хранилища данных. - Метод-конструктор
NewCache
для создания объекта кэша. - Метод
Set
для добавления значения в кэш по ключу. - Метод
Get
для получения значения из кэша по ключу. Если запрашиваемого элемента не найдено в мапе, то в качестве второго значения метод должен вернутьfalse
.
Пример работы методов:
func main() {
cache := NewCache[int]()
cache.Set("1", 1)
fmt.Println(cache.Get("1")) // 1 true
fmt.Println(cache.Get("2")) // 0 false
}
Решение
type Cache[T any] struct {
storage map[string]T
}
func NewCache[T any]() *Cache[T] {
return &Cache[T]{
storage: make(map[string]T),
}
}
func (c *Cache[T]) Set(key string, value T) {
c.storage[key] = value
}
func (c *Cache[T]) Get(key string) (T, bool) {
value, found := c.storage[key]
if !found {
return value, false
}
return value, true
}
Обобщенное множество
Напишите дженерик-реализацию множества – структуры данных, позволяющей хранить уникальные значения определённого типа в неупорядоченном виде. Программа должна состоять из следующих частей:
- Тип мапы с
comparable
ключами и значениями типаstruct{}
. Вместо пустой структуры, можно использовать типbool
, но такой подход будет менее эффективен, так как bool занимает больше памяти, чемstruct{}
. - Метод-конструктор
NewSet
для инициализации ключей мапы. В качестве параметра принимает список значенийcomparable
типа. - Метод
Add
для добавления значений в множество. В качестве параметра принимает список значенийcomparable
типа. - Метод
Contains
для проверки наличия значения во множестве. Возвращаетtrue
илиfalse
. - Метод
GetElements
для получения всех элементов множества. Возвращает слайсcomparable
типа.
Пример работы методов:
func main() {
set := NewSet(1, 2)
fmt.Println(set.Contains(9)) // false
set.Add(3, 4)
fmt.Println(set.GetElements()) // числа 1 2 3 4 в произвольном порядке
}
Решение
type Set[E comparable] map[E]struct{}
func NewSet[E comparable](values ...E) Set[E] {
set := Set[E]{}
for _, value := range values {
set[value] = struct{}{}
}
return set
}
func (set Set[E]) Add(values ...E) {
for _, val := range values {
set[val] = struct{}{}
}
}
func (set Set[E]) Contains(value E) bool {
_, found := set[value]
return found
}
func (set Set[E]) GetElements() []E {
var elements []E
for value := range set {
elements = append(elements, value)
}
return elements
}
Заключение
В этой статье мы познакомились с парадигмой обобщенного программирования, рассмотрели дженерики и их компоненты: type parameter
, type constraint
и type inference
. В конце закрепили материал на четырех практических задачах, которые наглядно демонстрируют применение дженериков.
В следующей части узнаем о способах хранения времени в компьютере и в языке Go, изучим основные функции пакета time и в конце решим парочку несложных задач
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
Комментарии