Введение в Generics
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 67 из 107.
До Go 1.18 написание переиспользуемого кода для различных типов означало либо дублирование функций, либо использование пустого интерфейса any с утверждениями типов. Дженерики решают эту проблему, позволяя писать функции и типы, которые работают с несколькими типами, сохраняя при этом полную типобезопасность.
Рассмотрим поиск минимального значения в срезе. Без использования дженериков вам понадобятся отдельные функции для каждого типа:
func MinInt(values []int) int {
min := values[0]
for _, v := range values {
if v < min {
min = v
}
}
return min
}
func MinFloat64(values []float64) float64 {
min := values[0]
for _, v := range values {
if v < min {
min = v
}
}
return min
}С помощью дженериков вы пишете логику один раз, используя параметр типа в квадратных скобках:
func Min[T int | float64](values []T) T {
min := values[0]
for _, v := range values {
if v < min {
min = v
}
}
return min
}[T int | float64] объявляет параметр типа T, который может быть либо int, либо float64. Теперь вы можете вызывать Min([]int{3, 1, 2}) или Min([]float64{3.5, 1.2}) с помощью одной и той же функции. Компилятор проверяет типы во время компиляции, поэтому вы получаете безопасность без накладных расходов во время выполнения.
В следующих уроках мы подробно рассмотрим параметры типов, ограничения и обобщенные структуры.
Задание
ЛегкоДавайте создадим анализатор оценок, который работает как с целыми числами, так и с числами с плавающей запятой, используя дженерики! Вы создадите одну универсальную функцию, которая сможет находить максимальное значение в срезе, избавляя от необходимости дублировать код для различных числовых типов.
Вы организуете свой код в двух файлах:
analyzer.go: Определите вашу универсальную функцию анализа.Создайте универсальную функцию
Max[T int | float64](values []T) T, которая находит и возвращает максимальное значение в срезе. Функция должна работать с типамиintиfloat64через параметр типаT.Также создайте универсальную функцию
Sum[T int | float64](values []T) T, которая вычисляет и возвращает сумму всех значений в срезе.main.go: Считайте входные данные и продемонстрируйте работу ваших универсальных функций с различными типами.Считайте индикатор типа (
intилиfloat), затем считайте количество значений, а после — сами значения. Обработайте значения в соответствии с типом, вызовите функцииMaxиSumс соответствующим типом и выведите результаты.Для целочисленного ввода (integer) выведите:
Max (int): [value] Sum (int): [value]Для ввода с плавающей запятой (float) выведите результат с одним десятичным знаком:
Max (float): [value] Sum (float): [value]
Будут предоставлены следующие входные данные:
- Строка 1: Индикатор типа (
intилиfloat) - Строка 2: Количество значений (целое число)
- Последующие строки: По одному значению на строку
Например, для ввода:
int
4
15
8
23
11Ваш вывод должен быть:
Max (int): 23
Sum (int): 57И для ввода:
float
3
4.5
9.2
6.8Ваш вывод должен быть:
Max (float): 9.2
Sum (float): 20.5Обратите внимание, как одни и те же универсальные функции обрабатывают как целочисленные данные, так и данные с плавающей запятой — параметр типа [T int | float64] позволяет компилятору генерировать типобезопасный код для каждого случая использования, в то время как вы пишете логику только один раз.
Шпаргалка
Дженерики (обобщения) позволяют писать переиспользуемый код, который работает с несколькими типами, сохраняя при этом типобезопасность. До Go 1.18 вам требовались отдельные функции для каждого типа или использование пустого интерфейса с утверждением типов.
Параметры типов объявляются в квадратных скобках и указывают, какие типы может принимать универсальная функция:
func Min[T int | float64](values []T) T {
min := values[0]
for _, v := range values {
if v < min {
min = v
}
}
return min
}Синтаксис [T int | float64] объявляет параметр типа T, который может быть либо int, либо float64. Оператор вертикальной черты | перечисляет допустимые типы.
Вы можете вызывать универсальные функции с различными типами:
Min([]int{3, 1, 2}) // Работает с int
Min([]float64{3.5, 1.2}) // Работает с float64Компилятор выполняет проверку типов во время компиляции, обеспечивая безопасность без накладных расходов во время выполнения.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Чтение индикатора типа
typeStr, _ := reader.ReadString('\n')
typeStr = strings.TrimSpace(typeStr)
// Чтение количества
countStr, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countStr))
if typeStr == "int" {
// Чтение целых значений
values := make([]int, count)
for i := 0; i < count; i++ {
line, _ := reader.ReadString('\n')
values[i], _ = strconv.Atoi(strings.TrimSpace(line))
}
// TODO: Вызвать Max и Sum со срезом целых чисел (int)
// TODO: Вывести результаты в формате "Max (int): [value]" и "Sum (int): [value]"
} else if typeStr == "float" {
// Чтение значений с плавающей точкой
values := make([]float64, count)
for i := 0; i < count; i++ {
line, _ := reader.ReadString('\n')
values[i], _ = strconv.ParseFloat(strings.TrimSpace(line), 64)
}
// TODO: Вызвать Max и Sum со срезом float64
// TODO: Вывести результаты с одним десятичным знаком в формате "Max (float): [value]" и "Sum (float): [value]"
}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool