Зачем в Go нужны дженерики и какие проблемы они решают?

Ответ

Дженерики (generics), появившиеся в Go 1.18, позволяют писать функции и структуры данных, которые могут работать с любыми типами, сохраняя при этом строгую проверку типов на этапе компиляции.

Основные проблемы, которые решают дженерики:

  1. Избавление от дублирования кода. Раньше для работы с разными типами приходилось писать отдельные функции для каждого из них (например, SortInts, SortStrings). Дженерики позволяют создать одну универсальную функцию.

  2. Типобезопасность без interface{}. До дженериков для написания обобщенного кода использовали interface{}. Это приводило к двум проблемам:

    • Потеря статической проверки типов: приходилось использовать утверждения типа (value.(string)), которые могли вызвать панику в рантайме.
    • Снижение производительности из-за упаковки (boxing) значений в интерфейсы.

Пример универсальной функции Map:

// T и U — это параметры типа (type parameters).
// any — это ограничение (constraint), означающее "любой тип".
func Map[T, U any](input []T, f func(T) U) []U {
    output := make([]U, len(input))
    for i, v := range input {
        output[i] = f(v)
    }
    return output
}

// Пример использования:
numbers := []int{1, 2, 3}
// Компилятор сам выводит типы, код типобезопасен.
strings := Map(numbers, strconv.Itoa) // Результат: []string{"1", "2", "3"}

Таким образом, дженерики делают код более чистым, переиспользуемым и безопасным, устраняя необходимость в бойлерплейте и небезопасных преобразованиях типов.

Ответ 18+ 🔞

А, дженерики в Go, блядь! Ну наконец-то, ёпта! Сидели мы тут, как дураки, с этими interface{}, которые нихуя не проверялись, пока код не запустишь и он тебе в сраку не влетит с паникой. А теперь — красота, ядрёна вошь!

Смотри, в чём была главная засада. Раньше, если тебе надо было отсортировать и числа, и строки, ты писал две отдельные функции, как последний идиот. SortInts, SortStrings — пиздец, одно название уже вызывало волнение ебать. Копипаста на ровном месте, чих-пых тебя в сраку!

А второй косяк — это вот этот ваш interface{}, который все так любят. Ну типа, "ой, я сейчас универсальную функцию напишу!" А на деле получается: суёшь туда что попало, компилятор молчит как партизан, а потом в рантайме — хрясь! — panic: interface conversion: interface {} is string, not int. И сидишь, чешешь репу: "Э, бошка, думай... где же я накосячил?"

И производительность, блядь, страдала! Потому что каждый раз значения в интерфейс упаковывались — это ж дополнительные телодвижения, нахер не нужные.

А теперь смотри, какая магия появилась в 1.18. Берём и пишем одну функцию на все случаи жизни:

// T и U — это типа как переменные, но для типов. Понял? Параметры типа, блядь.
// А `any` — это ограничение, по-русски: "бери любой тип, мне похуй".
func Map[T, U any](input []T, f func(T) U) []U {
    output := make([]U, len(input))
    for i, v := range input {
        output[i] = f(v)
    }
    return output
}

И теперь используем, не паримся:

numbers := []int{1, 2, 3}
// Компилятор сам всё видит и понимает. Никаких тебе танцев с бубном.
strings := Map(numbers, strconv.Itoa) // Получаем []string{"1", "2", "3"}, всё типобезопасно!

Вот и вся история. Теперь код чище, переиспользуется на ура, и компилятор стоит на страже, как суровый охранник в клубе, не пуская всякую хуйню. Красота, ёпта!