Дженерики в 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: Универсальный стек

Создайте структуру стека с дженериками, которая:

  1. Поддерживает любые типы данных
  2. Имеет методы Push и Pop
  3. Предоставляет информацию о размере
  4. Поддерживает проверку на пустоту

Ожидаемый результат:

  • Типобезопасный стек
  • Эффективные операции
  • Чистый интерфейс

Задание 2: Фильтрация слайсов

Реализуйте функцию для фильтрации слайсов:

  1. Принимает слайс любого типа
  2. Принимает функцию-предикат
  3. Возвращает новый слайс с отфильтрованными элементами
  4. Сохраняет порядок элементов

Ожидаемый результат:

  • Универсальная функция фильтрации
  • Гибкие критерии фильтрации
  • Эффективная работа

Задание 3: Карта с дженериками

Создайте структуру карты с дженериками:

  1. Поддерживает любые типы ключей и значений
  2. Имеет методы для добавления и получения элементов
  3. Поддерживает удаление элементов
  4. Предоставляет информацию о размере

Ожидаемый результат:

  • Типобезопасная карта
  • Эффективные операции
  • Гибкое использование

Что дальше?

В следующем уроке мы:

  • Изучим работу с горутинами
  • Познакомимся с каналами
  • Узнаем о синхронизации
  • Начнём писать параллельные программы

🎯 Цель урока: К концу этого урока вы должны уметь:

  • Создавать дженерик-функции
  • Использовать ограничения типов
  • Работать с дженерик-структурами
  • Применять дженерики с интерфейсами