Дженерики в Go: Гибкость и безопасность типов
В этом уроке мы изучим дженерики (Generics) в Go - мощный инструмент, который позволяет создавать гибкий и типобезопасный код. Дженерики были официально представлены в Go 1.18 и стали важной частью языка.
Почему важны дженерики?
Дженерики позволяют:
- Создавать универсальный код для разных типов
- Избегать дублирования кода
- Сохранять безопасность типов
- Повышать производительность
- Улучшать читаемость кода
💡 Интересный факт: До появления дженериков в Go разработчики часто использовали интерфейсы
interface{}или дублировали код для разных типов, что могло приводить к потере производительности и усложнению поддержки кода.
Основы дженериков
1. Простая дженерик-функция
package main
import "fmt"
// Обобщённая функция для сложения двух элементов
func Add[T any](a, b T) T {
return a + b
}
func main() {
// Работа с целыми числами
sumInt := Add(3, 4)
fmt.Printf("Сумма целых чисел: %d\n", sumInt)
// Работа с числами с плавающей точкой
sumFloat := Add(3.5, 2.1)
fmt.Printf("Сумма чисел с плавающей точкой: %.2f\n", sumFloat)
}
Объяснение:
[T any]объявляет параметр типаT, который может быть любым типом- Функция
Addпринимает два параметра типаTи возвращает значение типаT - Компилятор автоматически определяет тип на основе аргументов
Ожидаемый вывод:
Сумма целых чисел: 7
Сумма чисел с плавающей точкой: 5.60
2. Ограничения типов
package main
import "fmt"
// Ограничиваем типы до числовых
func Multiply[T int | float64](a, b T) T {
return a * b
}
func main() {
// Умножение целых чисел
productInt := Multiply(3, 4)
fmt.Printf("Произведение целых чисел: %d\n", productInt)
// Умножение чисел с плавающей точкой
productFloat := Multiply(3.5, 2.5)
fmt.Printf("Произведение чисел с плавающей точкой: %.2f\n", productFloat)
}
Объяснение:
[T int | float64]ограничивает типTтолько целыми числами и числами с плавающей точкой- Функция
Multiplyможет работать только с этими типами - Попытка использовать другие типы вызовет ошибку компиляции
Ожидаемый вывод:
Произведение целых чисел: 12
Произведение чисел с плавающей точкой: 8.75
Дженерики в структурах
1. Универсальный контейнер
package main
import "fmt"
// Универсальная структура для хранения данных
type Container[T any] struct {
value T
}
// Метод для получения значения
func (c Container[T]) GetValue() T {
return c.value
}
// Метод для установки значения
func (c *Container[T]) SetValue(value T) {
c.value = value
}
func main() {
// Контейнер для целых чисел
intContainer := Container[int]{value: 42}
fmt.Printf("Целочисленное значение: %d\n", intContainer.GetValue())
// Контейнер для строк
stringContainer := Container[string]{value: "Привет, мир!"}
fmt.Printf("Строковое значение: %s\n", stringContainer.GetValue())
// Изменение значения
intContainer.SetValue(100)
fmt.Printf("Новое значение: %d\n", intContainer.GetValue())
}
Объяснение:
Container[T]- универсальная структура, которая может хранить значение любого типа- Методы
GetValueиSetValueработают с типомT - Структура может быть использована с разными типами данных
Ожидаемый вывод:
Целочисленное значение: 42
Строковое значение: Привет, мир!
Новое значение: 100
2. Пара значений разных типов
package main
import "fmt"
// Структура для хранения пары значений разных типов
type Pair[T, U any] struct {
First T
Second U
}
func main() {
// Пара целое число и строка
pair1 := Pair[int, string]{
First: 42,
Second: "Ответ",
}
fmt.Printf("Пара 1: %d - %s\n", pair1.First, pair1.Second)
// Пара строка и число с плавающей точкой
pair2 := Pair[string, float64]{
First: "Пи",
Second: 3.14159,
}
fmt.Printf("Пара 2: %s - %.5f\n", pair2.First, pair2.Second)
}
Объяснение:
Pair[T, U]принимает два параметра типа- Каждое поле структуры может быть своего типа
- Позволяет создавать типобезопасные пары значений
Ожидаемый вывод:
Пара 1: 42 - Ответ
Пара 2: Пи - 3.14159
Дженерики с интерфейсами
1. Сравнимые типы
package main
import "fmt"
// Интерфейс для типов, поддерживающих сравнение
type Comparable interface {
int | float64 | string
}
// Функция для нахождения минимального значения
func Min[T Comparable](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
// Сравнение целых чисел
minInt := Min(3, 4)
fmt.Printf("Минимальное целое число: %d\n", minInt)
// Сравнение чисел с плавающей точкой
minFloat := Min(3.5, 2.1)
fmt.Printf("Минимальное число с плавающей точкой: %.2f\n", minFloat)
// Сравнение строк
minString := Min("яблоко", "банан")
fmt.Printf("Минимальная строка: %s\n", minString)
}
Объяснение:
Comparableограничивает типы до тех, которые поддерживают операцию сравнения- Функция
Minможет работать с любым типом, реализующимComparable - Обеспечивает типобезопасное сравнение
Ожидаемый вывод:
Минимальное целое число: 3
Минимальное число с плавающей точкой: 2.10
Минимальная строка: банан
2. Суммирование элементов
package main
import "fmt"
// Интерфейс для числовых типов
type Number interface {
int | float64
}
// Функция для суммирования элементов слайса
func Sum[T Number](numbers []T) T {
var sum T
for _, num := range numbers {
sum += num
}
return sum
}
func main() {
// Суммирование целых чисел
ints := []int{1, 2, 3, 4, 5}
sumInt := Sum(ints)
fmt.Printf("Сумма целых чисел: %d\n", sumInt)
// Суммирование чисел с плавающей точкой
floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
sumFloat := Sum(floats)
fmt.Printf("Сумма чисел с плавающей точкой: %.2f\n", sumFloat)
}
Объяснение:
Numberограничивает типы до числовых- Функция
Sumработает со слайсами любого числового типа - Обеспечивает типобезопасное суммирование
Ожидаемый вывод:
Сумма целых чисел: 15
Сумма чисел с плавающей точкой: 16.50
Практические задания
Задание 1: Универсальный стек
Создайте структуру стека с дженериками, которая:
- Поддерживает любые типы данных
- Имеет методы Push и Pop
- Предоставляет информацию о размере
- Поддерживает проверку на пустоту
Ожидаемый результат:
- Типобезопасный стек
- Эффективные операции
- Чистый интерфейс
Задание 2: Фильтрация слайсов
Реализуйте функцию для фильтрации слайсов:
- Принимает слайс любого типа
- Принимает функцию-предикат
- Возвращает новый слайс с отфильтрованными элементами
- Сохраняет порядок элементов
Ожидаемый результат:
- Универсальная функция фильтрации
- Гибкие критерии фильтрации
- Эффективная работа
Задание 3: Карта с дженериками
Создайте структуру карты с дженериками:
- Поддерживает любые типы ключей и значений
- Имеет методы для добавления и получения элементов
- Поддерживает удаление элементов
- Предоставляет информацию о размере
Ожидаемый результат:
- Типобезопасная карта
- Эффективные операции
- Гибкое использование
Что дальше?
В следующем уроке мы:
- Изучим работу с горутинами
- Познакомимся с каналами
- Узнаем о синхронизации
- Начнём писать параллельные программы
🎯 Цель урока: К концу этого урока вы должны уметь:
- Создавать дженерик-функции
- Использовать ограничения типов
- Работать с дженерик-структурами
- Применять дженерики с интерфейсами