пятница, 20 мая 2022 г.

Начало работы с дженериками в Golang

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

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

Предпосылки

  • Установка Go 1.18 или более поздней версии.
  • Инструмент для редактирования вашего кода. Любой текстовый редактор, который у вас есть, будет работать нормально.
  • Командный терминал. Go хорошо работает с любым терминалом в Linux и Mac, а также с PowerShell или cmd в Windows.

Создайте папку для своего кода

Для начала создайте папку для кода, который вы будете писать.

1. Откройте командную строку и перейдите в свой домашний каталог.

В Linux или Mac:

$ cd

В Windows:

C:\> cd %HOMEPATH%

В остальной части руководства в качестве подсказки будет отображаться символ $. Используемые вами команды будут работать и в Windows.

2. В командной строке создайте каталог для своего кода с именем generics.

$ mkdir generics
$ cd generics

3. Создайте модуль для хранения вашего кода.

Запустите команду инициализации go mod, указав путь к модулю вашего нового кода.

$ go mod init example/generics
go: creating new go.mod: module example/generics

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

Далее вы добавите простой код для работы с картами.

Добавьте неуниверсальные функции

На этом шаге вы добавите две функции, каждая из которых суммирует значения карты и возвращает итог.

Вы объявляете две функции вместо одной, потому что работаете с картами двух разных типов: в одной хранятся значения int64, а в другой хранятся значения float64.

Напишите код

1. Используя текстовый редактор, создайте файл с именем main.go в каталоге generics. Вы будете писать свой код Go в этом файле.

2. В файл main.go в верхней части файла вставьте следующее объявление пакета.

package main

Автономная программа (в отличие от библиотеки) всегда находится в пакете main.

3. Под объявлением пакета вставьте следующие два объявления функций.

// SumInts суммирует значения m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats суммирует значения m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

В этом коде вы объявляете две функции для сложения значений карты и возврата суммы.

SumFloats преобразует строку в значения float64.

SumInts преобразует строку в значения int64.

4. В верхней части main.go под объявлением пакета вставьте следующую main функцию, чтобы инициализировать две карты и использовать их в качестве аргументов при вызове функций, которые вы объявили на предыдущем шаге.

func main() {
    // Инициализировать карту для целочисленных значений
    ints := map[string]int64{
        "first":  34,
        "second": 12,
    }

    // Инициализируем карту для значений с плавающей запятой
    floats := map[string]float64{
        "first":  35.98,
        "second": 26.99,
    }

    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))
}

В этом коде вы:

  • Инициализируете карту значений float64 и карту значений int64, каждая с двумя записями.
  • Вызывате две объявленные ранее функции, чтобы найти сумму значений каждой карты.
  • Распечатываете результат.

5. В верхней части main.go, прямо под объявлением пакета, импортируйте пакет, который вам понадобится для поддержки кода, который вы только что написали.

Первые строки кода должны выглядеть так:

package main

import "fmt"

6. Сохраните main.go.

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Non-Generic Sums: 46 and 62.97

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

Добавьте общую функцию для обработки нескольких типов

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

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

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

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

В то время как ограничение параметра типа обычно представляет набор типов, во время компиляции параметр типа обозначает один тип — тип, предоставляемый вызывающим кодом в качестве аргумента типа. Если тип аргумента типа не разрешен ограничением параметра типа, код не будет компилироваться.

Имейте в виду, что параметр типа должен поддерживать все операции, выполняемые над ним универсальным кодом. Например, если бы код вашей функции пытался выполнить строковые операции (например, индексацию) над параметром типа, ограничение которого включало числовые типы, код не скомпилировался бы.

В коде, который вы собираетесь написать, вы будете использовать ограничение, допускающее целочисленные типы или типы с плавающей запятой.

Напишите код

1. Под двумя функциями, которые вы добавили ранее, вставьте следующую общую функцию.

// SumIntsOrFloats суммирует значения карты m.
// Поддерживает как int64, так и float64 
// как типы для значений карты.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

В этом коде вы:

  • Объявите функцию SumIntsOrFloats с двумя параметрами типа (в квадратных скобках), K и V, и одним аргументом, который использует параметры типа, m типа map[K]V. Функция возвращает значение типа V.
  • Укажете для параметра типа K сопоставимое ограничение типа. Предназначенное специально для таких случаев, сопоставимое ограничение уже объявлено в Go. Он допускает любой тип, значения которого можно использовать в качестве операнда операторов сравнения == и !=. Go требует, чтобы ключи сопоставления были сопоставимы. Поэтому необходимо объявить K сопоставимым, чтобы вы могли использовать K в качестве ключа в переменной карты. Это также гарантирует, что вызывающий код использует допустимый тип для ключей сопоставления.
  • Укажете для параметра типа V ограничение, представляющее собой объединение двух типов: int64 и float64. Использование | задает объединение двух типов, что означает, что это ограничение допускает любой тип. Любой тип будет разрешен компилятором в качестве аргумента в вызывающем коде.
  • Укажете, что аргумент m имеет тип map[K]V, где K и V — это типы, уже указанные для параметров типа. Обратите внимание, что мы знаем, что map[K]V является допустимым типом карты, потому что K является сопоставимым типом. Если бы мы не объявили K сопоставимым, компилятор отклонил бы ссылку на map[K]V.

2. В main.go под уже имеющимся кодом вставьте следующий код.

fmt.Printf("Generic Sums: %v and %v\n",
    SumIntsOrFloats[string, int64](ints),
    SumIntsOrFloats[string, float64](floats))

В этом коде вы:

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

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97

Чтобы выполнить ваш код, в каждом вызове компилятор заменял параметры типа конкретными типами, указанными в этом вызове.

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

Удалить аргументы типа при вызове универсальной функции

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

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

Обратите внимание, что это не всегда возможно. Например, если вам нужно вызвать универсальную функцию без аргументов, вам нужно будет включить аргументы типа в вызов функции.

Напишите код

В main.go под уже имеющимся кодом вставьте следующий код.

fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))

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

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97

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

Объявите ограничение типа

В этом последнем разделе вы переместите ограничение, которое вы определили ранее, в его собственный интерфейс, чтобы вы могли повторно использовать его в нескольких местах. Объявление ограничений таким образом помогает упростить код, например, когда ограничение более сложное.

Вы объявляете ограничение типа как интерфейс. Ограничение позволяет любому типу реализовать интерфейс. Например, если вы объявляете интерфейс ограничения типа с тремя методами, а затем используете его с параметром типа в универсальной функции, аргументы типа, используемые для вызова функции, должны иметь все эти методы.

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

Напишите код

1. Чуть выше main функции, сразу после операторов импорта, вставьте следующий код, чтобы объявить ограничение типа.

type Number interface {
    int64 | float64
}

В этом коде вы:

  • Объявите тип интерфейса Number для использования в качестве ограничения типа.
  • Объявите объединение int64 и float64 внутри интерфейса.
    По сути, вы перемещаете объединение из объявления функции в ограничение нового типа. Таким образом, если вы хотите ограничить параметр типа значением int64 или float64, вы можете использовать это ограничение числового типа вместо записи int64 | float64.

2. Под уже имеющимися функциями вставьте следующую общую функцию SumNumbers.

// SumNumbers суммирует значения карты m. 
// Поддерживает целые числа и 
// числа с плавающей точкой как значения карты.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

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

3. В main.go под уже имеющимся кодом вставьте следующий код.

fmt.Printf("Generic Sums with Constraint: %v and %v\n",
    SumNumbers(ints),
    SumNumbers(floats))

В этом коде вы вызовете SumNumbers с каждой картой, выводя сумму из значений каждой.

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

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97

Завершенный код

Вы можете запустить эту программу на игровой площадке Go. На игровой площадке просто нажмите кнопку Run.

package main

import "fmt"

type Number interface {
    int64 | float64
}

func main() {
    // Инициализируем карту для целочисленных значений
    ints := map[string]int64{
        "first": 34,
        "second": 12,
    }

    // Инициализируем карту для значений с плавающей точкой
    floats := map[string]float64{
        "first": 35.98,
        "second": 26.99,
    }

    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))

    fmt.Printf("Generic Sums: %v and %v\n",
        SumIntsOrFloats[string, int64](ints),
        SumIntsOrFloats[string, float64](floats))

    fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
        SumIntsOrFloats(ints),
        SumIntsOrFloats(floats))

    fmt.Printf("Generic Sums with Constraint: %v and %v\n",
        SumNumbers(ints),
        SumNumbers(floats))
}

// SumInts суммирует значения m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats суммирует значения m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

// SumIntsOrFloats суммирует значения карты m. 
// Поддерживает как числа с плавающей запятой, так и целые числа
// как значения карты.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

// SumNumbers суммирует значения карты m. 
// Поддерживает как числа с плавающей запятой, так и целые числа
// как значения карты.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

Вывод:

Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97


Читайте также:


Комментариев нет:

Отправить комментарий