В этой статье мы обсудим универсальные реализации Golang.

Генераторы Golang, вероятно, были самой востребованной функцией в сообществе Golang с версии 1.0, когда они объявили, что собираются добавить дженерики в Go, начались дискуссии о реализации дженериков, мы обсудим две самые популярные реализации и, наконец, текущую реализацию Go1.18.

Трафарет

Использование трафарета — это один из подходов, которые мы можем использовать при реализации дженериков в компиляторе, в этом подходе, если у нас есть универсальная функция, такая как

func max[T comparable](a, b T) bool {
    if a > b { 
        return a 
    } else {
        return b
    }
}

для каждого вызова в нашей кодовой базе приходил компилятор и генерировал новый экземпляр этой функции только для этого вызова, например:

max[int](1,2) // this will generate something like max_int_1
max[float32)(1.0, 2.0) // this will generate something like max_float32_1
max[int](3,4) // this will also generate a new max_int_2

Как вы можете видеть, в этом подходе мы должны генерировать экземпляр функции для каждого вызова, и даже когда экземпляры одинаковы или похожи, у нас есть дубликаты.

Обратите внимание, что использование трафаретов обеспечивает наилучшую производительность во время выполнения, но требует длительного времени компиляции и больших двоичных файлов. С++ использует этот подход.

Словари

использование словарей — еще один подход к использованию дженериков в компиляторе. Словари — это полная противоположность Stenciling, так как они будут иметь единственный экземпляр функции, и вся необходимая информация о параметрах типа передается в структуру словаря этой функции.

func max[T comparable](a, b T) bool {
    if a > b { 
        return a 
    } else {
        return b
    }
}

рассмотрите приведенный выше код как общую максимальную функцию.

max[int](1,2)
max[float32)(1.0, 2.0)
max[int](3,4)

все вышеперечисленные вызовы относятся к одному и тому же экземпляру функции max, и различаются только данные словаря, например, для первого вызова словарь содержит тип int и некоторую другую информацию о том, как внутренние функции, такие как make/len/new/etc..., должны работать для целых чисел.

Этот подход имеет самую низкую производительность во время выполнения, поскольку для каждого универсального вызова необходимо создать и заполнить этот словарь, а функциям также необходимо обращаться к этому словарю для большинства своих операций. Этот подход генерирует меньше ассемблерного кода и меньшие двоичные файлы, но код компилятора намного сложнее, чем Stenciling .

Перейти к реализации

Реализация Golang в основном представляет собой золотую середину между двумя вышеуказанными методами. Генераторы Go знакомят нас с концепцией под названием GCShape, которая в основном представляет собой группу схожих типов, типов, имеющих один и тот же базовый тип, или типов, являющихся типами-указателями, которые находятся в одной группе GCShape, например:

type a int
type b int
type c = int
// above are same GCShape

помните, что типы, которые находятся в одном и том же GCShape, должны иметь абсолютно одинаковое поведение для всех внутренних операций компилятора, например, для оператора + они должны вести себя одинаково, поэтому даже int16 и int32 не находятся в одном и том же GCShape. Компилятор Go создает тип GCShape, когда видит параметр типа, который не принадлежит ни одному GCShape, поэтому, например, для string он создаст go.shape.string_0, обратите внимание, что 0 означает, что это первый параметр типа.

Компилятор Go выполняет трафарет для каждого GCShape вместо каждого универсального вызова, а затем на каждом сайте вызова создает словарь, содержащий информацию о типе для этого конкретного типа, а затем вызывает функцию, поэтому, когда типы находятся в одном и том же GCShape, они используют один и тот же экземпляр функции.

поэтому для той же функции max у нас было

max[int](1,2)
max[int](3,4)

вышеприведенные вызовы функций совместно используют один и тот же экземпляр функции. вызов функции ниже также использует тот же экземпляр, поскольку базовый тип — int, и мы можем выполнять все внутренние операции компилятора так же, как и сам int, обратите внимание, что словарь, созданный для вызовов выше, отличается от словаря, созданного для вызова ниже, поскольку они разные типов, но поскольку они находятся в одном GCShape, они могут совместно использовать один и тот же экземпляр универсальной функции.

type a int
max[a](1,2)

Вы также можете увидеть процесс реализации Golang на картинке ниже.

Как видите, реализация дженериков в Go1.18 — лучшее из обоих миров, она быстро компилируется и не создает толстых двоичных файлов. У него есть ограничения, как было объявлено в примечаниях к выпуску Go1.18.

Обобщения — самая большая функция и изменение языка Go. С каждым днем ​​мы должны видеть все больше и больше случаев использования дженериков, поскольку они делают возможным код, который было невозможно написать.