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

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

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

Вы пройдете через следующие разделы:

  1. Создание папки для вашего кода.
  2. Добавление недженериковых функций.
  3. Добавление дженериковой функции для обработки нескольких типов.
  4. Удаление аргументов типа при вызове дженериковой функции.
  5. Объявление ограничения типа.

Примечание: Другие руководства смотрите в разделе Руководства.

Примечание: Если хотите, вы можете использовать площадку Go в режиме "Go dev branch" для редактирования и запуска вашей программы.

Предварительные требования

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

Создание папки для вашего кода

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

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

    На Linux или Mac:

    <code>$ cd
    </code>

    На Windows:

    <code>C:\> cd %HOMEPATH%
    </code>

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

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

    <code>$ mkdir generics
    $ cd generics
    </code>
  3. Создайте модуль для хранения вашего кода.

    Выполните команду go mod init, указав путь модуля вашего нового кода.

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

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

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

Добавление недженериковых функций

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

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

Напишите код

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

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

    <code>package main
    </code>

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

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

    <code>// 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
    }
    </code>

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

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

    <code>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))
    }
    </code>

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

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

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

    <code>package main
    import "fmt"
    </code>
  6. Сохраните main.go.

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

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

<code>$ go run .
Non-Generic Sums: 46 and 62.97
</code>

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

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

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

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

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

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

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

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

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

Напишите код

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

    <code>// 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
    }
    </code>

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

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

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

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

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

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

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

    • Выводите суммы, возвращенные функцией.

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

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

<code>$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
</code>

Для запуска вашего кода в каждом вызове компилятор заменил параметры типа конкретными типами, указанными в этом вызове.

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

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

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

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

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

Напишите код

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

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

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

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

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

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

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

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

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

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

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

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

Напишите код

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

    <code>type Number interface {
      int64 | float64
    }
    </code>

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

    • Объявляете тип интерфейса Number для использования в качестве ограничения типа.

    • Объявляете объединение int64 и float64 внутри интерфейса.

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

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

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

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

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

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

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

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

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

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

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

<code>$ 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
</code>

Заключение

Отлично! Вы только что познакомились с дженериками в Go.

Рекомендуемые следующие темы:

Полный код

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

<code>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
}
</code>
GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang