Показаны сообщения с ярлыком Дженерики в Golang. Показать все сообщения
Показаны сообщения с ярлыком Дженерики в Golang. Показать все сообщения

пятница, 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


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


понедельник, 2 мая 2022 г.

Введение в использование дженериков в Golang

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

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

Дженерики добавляют в язык три новых важных вещи:

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

Параметры типа

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

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

func Min(x, y float64) float64 {
    if x < y {
        return x
    }
    return y
}

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

import "golang.org/x/exp/constraints"

func GMin[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

Теперь можно вызвать эту функцию с аргументом типа, написав вызов вида

x := GMin[int](2, 3)

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

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

fmin := GMin[float64]
m := fmin(2.71, 3.14)

экземпляр GMin[float64] создает то, что фактически является нашей исходной функцией Min с плавающей запятой, и мы можем использовать это в вызове функции.

Параметры типа также могут использоваться с типами.

type Tree[T interface{}] struct {
    left, right *Tree[T]
    value       T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

var stringTree Tree[string]

Здесь дерево универсального типа хранит значения параметра типа T. Универсальные типы могут иметь методы, такие как Lookup в этом примере. Чтобы использовать универсальный тип, необходимо создать его экземпляр; Tree[string] — это пример создания экземпляра Tree с типом аргумента string.

Наборы типов

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

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

Точно так же наборы параметров типа имеют тип для каждого параметра типа. Поскольку параметр типа сам по себе является типом, типы параметров типа определяют наборы типов. Этот метатип называется ограничением типа (type constraint).

В универсальном GMin ограничение типа импортируется из пакета constraints. Ограничение Ordered описывает набор всех типов со значениями, которые можно упорядочить или, другими словами, сравнить с оператором < (или <= , > и т. д.). Ограничение гарантирует, что в GMin могут быть переданы только типы с упорядочиваемыми значениями. Это также означает, что в теле функции GMin значения параметра этого типа могут использоваться для сравнения с оператором <.

В Go ограничения типов должны быть интерфейсами. То есть тип интерфейса может использоваться как тип значения, а также может использоваться как метатип. Интерфейсы определяют методы, поэтому очевидно, что мы можем выразить ограничения типов, которые требуют наличия определенных методов. Но constraints.Ordered — это тоже интерфейсный тип, а оператор < не является методом.

Чтобы это работало, мы смотрим на интерфейсы по-новому.

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

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

Однако для наших целей представление набора типов имеет преимущество перед представлением набора методов: мы можем явно добавлять типы в набор и, таким образом, управлять набором типов новыми способами.

Синтаксис типов интерфейса был расширен, чтобы это работало. Например, interface{ int|string|bool } определяет набор типов, содержащий типы int, string и bool.

Другими словами, этому интерфейсу удовлетворяют только int, string или bool.

Теперь давайте посмотрим на фактическое определение contraints.Ordered:

type Ordered interface {
    Integer|Float|~string
}

В этом объявлении говорится, что интерфейс Ordered представляет собой набор всех типов целых чисел, чисел с плавающей запятой и строк. Вертикальная черта обозначает объединение типов (или наборов типов в данном случае). Integer и Float — это типы интерфейса, которые аналогичным образом определены в пакете contraints. Обратите внимание, что нет методов, определенных интерфейсом Ordered.

Для ограничений типа мы обычно не заботимся о конкретном типе, таком как string; нас интересуют все типы строк. Именно для этого используется токен ~. Выражение ~string означает набор всех типов, базовым типом которых является string. Это включает в себя сам тип string, а также все типы, объявленные с такими определениями, как type MyString string.

Конечно, мы по-прежнему хотим указывать методы в интерфейсах, и мы хотим быть обратно совместимыми. В Go 1.18 интерфейс может содержать методы и встроенные интерфейсы, как и раньше, но он также может включать неинтерфейсные типы, объединения (union) и наборы базовых типов.

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

Интерфейсам, используемым в качестве ограничений, могут быть присвоены имена (например, Ordered), или они могут быть литеральными интерфейсами, встроенными в список параметров типа. Например:

[S interface{~[]E}, E interface{}]

Здесь S должен быть типом среза, тип элемента которого может быть любым.

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

[S ~[]E, E interface{}]

Поскольку пустой интерфейс распространен в списках параметров типа и в обычном коде Go, в этом отношении Go 1.18 вводит новый предварительно объявленный идентификатор any в качестве псевдонима для пустого типа интерфейса. При этом мы приходим к этому идиоматическому коду:

[S ~[]E, E any]

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

Вывод типа

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

Вывод типа аргумента функции

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

func GMin[T constraints.Ordered](x, y T) T { ... }

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

var a, b, m float64

m = GMin[float64](a, b) // явный аргумент типа

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

var a, b, m float64

m = GMin(a, b) // нет аргумента типа

Это работает путем сопоставления типов аргументов a и b с типами параметров x и y.

Этот тип вывода, который выводит аргументы типа из типов аргументов функции, называется выводом типа аргумента функции.

Вывод типа аргумента функции работает только для параметров типа, которые используются в параметрах функции, а не для параметров типа, используемых только в результатах функции или только в теле функции. Например, это не относится к таким функциям, как MakeT[T any]() T, которые используют только T для результата.

Вывод типа ограничения

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

// Scale возвращает копию s с каждым элементом, умноженным на c.
// У этой реализации есть проблема, как мы увидим.
func Scale[E constraints.Integer](s []E, c E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

Это общая функция, которая работает для среза любого целочисленного типа.

Теперь предположим, что у нас есть многомерный тип Point, где каждый Point — это просто список целых чисел, задающих координаты точки. Естественно, этот тип будет иметь несколько методов.

type Point []int32

func (p Point) String() string {
    // Детали не важны.
}

Иногда мы хотим масштабировать Point. Поскольку Point — это просто срез целых чисел, мы можем использовать функцию Scale, которую мы написали ранее:

// ScaleAndPrint удваивает Point и печатает ее.
func ScaleAndPrint(p Point) {
    r := Scale(p, 2)
    fmt.Println(r.String()) // НЕ КОМПИЛИРУЕТСЯ
}

К сожалению, это не компилируется, выдавая ошибку типа r.String undefined (тип []int32 не имеет поля или метода String).

Проблема в том, что функция Scale возвращает значение типа []E, где E — тип элемента среза аргумента. Когда мы вызываем Scale со значением типа Point, базовым типом которого является []int32, мы возвращаем значение типа []int32, а не типа Point. Это следует из того, как написан универсальный код, но это не то, что нам нужно.

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

// Scale возвращает копию s с каждым элементом, умноженным на c.
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

Мы ввели новый параметр типа S, который является типом аргумента среза. Мы наложили на него ограничения таким образом, что базовым типом является S, а не []E, а тип результата теперь равен S. Поскольку E ограничено целым числом, эффект тот же, что и раньше: первый аргумент должен быть срез некоторого целочисленного типа. Единственное изменение в теле функции заключается в том, что теперь мы передаем S, а не []E, когда вызываем make.

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

Но справедливо спросить: почему можно написать вызов Scale без передачи явных аргументов типа? То есть, почему мы можем писать Scale(p, 2) без аргументов типа вместо того, чтобы писать Scale[Point, int32](p, 2)? Наша новая функция Scale имеет два параметра типа, S и E. При вызове Scale без передачи каких-либо аргументов типа вывод типа аргумента функции, описанный выше, позволяет компилятору сделать вывод, что аргумент типа для S — это Point. Но функция также имеет параметр типа E, который является типом коэффициента умножения c. Соответствующий аргумент функции равен 2, и поскольку 2 является нетипизированной константой, вывод типа аргумента функции не может вывести правильный тип для E (в лучшем случае он может вывести тип по умолчанию для 2, который является int и который будет неправильным). Вместо этого процесс, посредством которого компилятор делает вывод о том, что аргумент типа для E является типом элемента среза, называется выводом типа ограничения.

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

Обычно это применяется, когда одно ограничение использует форму ~type для некоторого типа, где этот тип записывается с использованием других параметров типа. Мы видим это в примере Scale. S — это ~[]E, за которым следует тип []E, записанный в терминах другого параметра типа. Если мы знаем аргумент типа для S, мы можем вывести аргумент типа для E. S — это тип среза, а E — тип элемента этого среза.

Вывод типа на практике

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

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

Заключение

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


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


пятница, 8 апреля 2022 г.

Релиз Go 1.18

Релиз Go, версия 1.18, является важным релизом, включающим изменения в языке, реализации цепочки инструментов, среды выполнения и библиотек. Go 1.18 выходит через семь месяцев после Go 1.17. Как всегда, релиз поддерживает обещание совместимости Go 1. Ожидается, что почти все программы Go продолжат компилироваться и работать, как и прежде.

Изменения в языке


Дженерики

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

Эти новые языковые изменения потребовали большого количества нового кода, который не подвергался серьезному тестированию в производственных условиях. Это произойдет только тогда, когда все больше людей будут писать и использовать общий код (код с дженериками). Считается, что эта часть реализована хорошо и качественно. Однако, в отличие от большинства аспектов Go, не возможно подтвердить это убеждение реальным опытом. Поэтому, хотя поощряется использование универсальных шаблонов (генериков) там, где это имеет смысл, соблюдайте осторожность при развертывании универсального кода (кода с дженериками) в производственной среде.

Хотя считается, что новые функции языка хорошо разработаны и четко определены, возможно, допущены ошибки. Важно подчеркнуть, что гарантия совместимости с Go 1 гласит: "Если возникает необходимость устранить несоответствие или неполноту в спецификации, решение проблемы может повлиять на смысл или законность существующих программ. Мы оставляем за собой право решать такие проблемы, включая обновление реализации". В нем также говорится: "Если в компиляторе или библиотеке есть ошибка, нарушающая спецификацию, программа, зависящая от ошибочного поведения, может сломаться, если ошибка будет исправлена. Мы оставляем за собой право исправлять такие ошибки". Другими словами, возможно, что будет код, использующий дженерики, который будет работать с релизом 1.18, но сломается в более поздних релизах. Не планируется и не ожидается подобных изменений. Однако поломка программ 1.18 в будущих релизах может оказаться необходимой по причинам, которые сегодня не возможно предвидеть. Будет сведена к минимуму любая такая поломка, но не возможно гарантировать, что ее не будет совсем.

Ниже приведен список наиболее заметных изменений.

  • Синтаксис объявлений функций и типов теперь принимает параметры типа.
  • Параметризованные функции и типы могут быть созданы, если за ними следует список аргументов типа в квадратных скобках.
  • В набор операторов и знаков препинания добавлен новый токен ~.
  • Синтаксис типов интерфейсов теперь позволяет встраивать произвольные типы (а не только имена типов интерфейсов), а также элементы типа union и ~T. Такие интерфейсы могут использоваться только как ограничения типа. Интерфейс теперь определяет набор типов, а также набор методов.
  • Новый предварительно объявленный идентификатор any является псевдонимом для пустого интерфейса. Его можно использовать вместо interface{}.
  • Новый предварительно объявленный идентификатор compare — это интерфейс, обозначающий набор всех типов, которые можно сравнивать с помощью == или !=. Его можно использовать только как (или встроенное) ограничение типа.

Есть три экспериментальных пакета, использующих дженерики, которые могут оказаться полезными. Эти пакеты находятся в репозитории x/exp; их API не покрывается гарантией Go 1 и может меняться по мере того, как накапливается больше опыта работы с дженериками.

golang.org/x/exp/constraints
Ограничения, полезные для универсального кода, такие как constraints.Ordered.

golang.org/x/exp/slices
Набор универсальных функций, которые работают со срезами элементов любого типа.

golang.org/x/exp/maps
Набор универсальных функций, которые работают с картами любого типа ключа или элемента.

Текущая реализация дженериков имеет следующие известные ограничения:

  • Компилятор Go не может обрабатывать объявления типов внутри универсальных функций или методов. Планируется обеспечить поддержку этой функции в Go 1.19.
  • Компилятор Go не принимает аргументы типа параметра типа с предварительно объявленными функциями real, imag и complex. Планируется убрать это ограничение в Go 1.19.
  • Компилятор Go поддерживает только вызов метода m для значения x типа параметра типа P, если m явно объявлен интерфейсом ограничения P. Точно так же значения метода x.m и выражения метода P.m также поддерживаются только в том случае, если m явно объявлен P, даже если m может находиться в наборе методов P в силу того факта, что все типы в P реализуют m. Планируется убрать это ограничение в Go 1.19.
  • Компилятор Go не поддерживает доступ к полю структуры x.f, где x имеет тип параметра типа, даже если все типы в наборе типов параметра типа имеют поле f. Планируется убрать это ограничение в Go 1.19.
  • Встраивание параметра типа или указателя на параметр типа в качестве безымянного поля в типе структуры не допускается. Точно так же не допускается встраивание параметра типа в тип интерфейса. Будут ли они когда-либо разрешены, в настоящее время неясно.
  • Элемент union с более чем одним термином не может содержать тип интерфейса с непустым набором методов. Будет ли это когда-либо разрешено, пока неясно.

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

Исправление ошибок

Компилятор Go 1.18 теперь правильно сообщает об объявленных, но не используемых ошибках для переменных, которые установлены внутри литерала функции, но никогда не используются. До Go 1.18 компилятор не сообщал об ошибке в таких случаях. Это устраняет давнюю проблему с компилятором. В результате этого изменения (возможно, некорректные) программы могут больше не компилироваться. Необходимое исправление простое: исправить программу, если она действительно была неправильной, или использовать неверную переменную, например, присвоив ей пустой идентификатор _. Поскольку go vet всегда указывал на эту ошибку, количество затронутых программ, вероятно, очень мало.

Компилятор Go 1.18 теперь сообщает о переполнении при передаче константного выражения руны, такого как '1' << 32, в качестве аргумента предварительно объявленным функциям print и println, что соответствует поведению пользовательских функций. До Go 1.18 компилятор не сообщал об ошибке в таких случаях, а молча принимал такие константные аргументы, если они соответствовали типу int64. В результате этого изменения (возможно, некорректные) программы могут больше не компилироваться. Необходимое исправление простое: исправить программу, если она на самом деле была неправильной, или явно преобразовать неверный аргумент в правильный тип. Поскольку go vet всегда указывал на эту ошибку, количество затронутых программ, вероятно, очень мало.

Порты


AMD64

Go 1.18 представляет новую переменную среды GOAMD64, которая выбирает во время компиляции минимальную целевую версию архитектуры AMD64. Допустимые значения: v1, v2, v3 или v4. Каждый более высокий уровень требует и использует дополнительные функции процессора.

Переменная среды GOAMD64 по умолчанию имеет значение v1.

RISC-V

64-разрядная архитектура RISC-V в Linux (порт linux/riscv64) теперь поддерживает режимы сборки c-archive и c-shared.

Linux

Для Go 1.18 требуется ядро Linux версии 2.6.32 или выше.

Windows

Порты windows/arm и windows/arm64 теперь поддерживают некооперативное вытеснение, предоставляя эту возможность всем четырем портам Windows, что должно исправить тонкие ошибки, возникающие при вызове функций Win32, которые блокируются в течение длительных периодов времени.

iOS

На iOS (порт ios/arm64) и симуляторе iOS, работающем на macOS на базе AMD64 (порт ios/amd64), для Go 1.18 теперь требуется iOS 12 или новее; поддержка предыдущих версий прекращена.

FreeBSD

Go 1.18 — это последний выпуск, поддерживаемый FreeBSD 11.x, срок службы которого уже истек. Go 1.19 потребует FreeBSD 12.2+ или FreeBSD 13.0+. Для FreeBSD 13.0+ потребуется ядро с установленным параметром COMPAT_FREEBSD12 (это значение по умолчанию).

Инструменты


Фаззинг

Go 1.18 включает в себя реализацию фаззинга.

Имейте в виду, что фаззинг может потреблять много памяти и может повлиять на производительность вашей машины во время ее работы. Также имейте в виду, что механизм фаззинга записывает значения, которые расширяют тестовое покрытие, в каталог кэша фаззинга в $GOCACHE/fuzz во время его работы. В настоящее время нет ограничений на количество файлов или общее количество байтов, которые могут быть записаны в нечеткий кэш, поэтому он может занимать большой объем памяти (возможно, несколько ГБ).

Команда go


go get

go get больше не собирает и не устанавливает пакеты в режиме с поддержкой модулей. go get теперь предназначен для настройки зависимостей в go.mod. По сути, флаг -d всегда включен. Чтобы установить последнюю версию исполняемого файла вне контекста текущего модуля, используйте go install example.com/cmd@latest. Вместо последней версии можно использовать запрос любой версии. Эта форма go install была добавлена ​​в Go 1.16, поэтому в проектах, поддерживающих более старые версии, может потребоваться предоставить инструкции по установке как для go install, так и для go get. go get теперь сообщает об ошибке при использовании вне модуля, так как нет файла go.mod для обновления. В режиме GOPATH (с GO111MODULE=off) go get по-прежнему собирает и устанавливает пакеты, как и раньше.

Автоматические обновления go.mod и go.sum

go mod graph, go mod vendor, go mod verify и go mod why подкоманды больше не обновляют автоматически файлы go.mod и go.sum. (Эти файлы можно явно обновить с помощью go get, go mod tidy или go mod download.)

go version

Команда go теперь встраивает информацию о контроле версий в двоичные файлы. Она включает текущую проверенную версию, время коммита и флаг, указывающий, присутствуют ли отредактированные или неотслеживаемые файлы. Информация о контроле версий встраивается, если команда go вызывается в каталоге в репозитории Git, Mercurial, Fossil или Bazaar, а main пакет и содержащий его main модуль находятся в одном репозитории. Эту информацию можно опустить, используя флаг -buildvcs=false.

Кроме того, команда go встраивает информацию о сборке, включая теги сборки и инструмента (установленные с помощью -tags), флаги компилятора, ассемблера и компоновщика (например, -gcflags), включено ли cgo, и если да, то значения переменные среды cgo (например, CGO_CFLAGS). Информация о VCS и сборке может быть прочитана вместе с информацией о модуле с помощью go version -m file или runtime/debug.ReadBuildInfo (для текущего исполняемого бинарного файла) или нового пакета debug/buildinfo.

Базовый формат данных встроенной информации о сборке может меняться с новыми релизами go, поэтому более старая версия go может не обрабатывать информацию о сборке, созданную с помощью более новой версии go. Чтобы прочитать информацию о версии из двоичного файла, созданного с помощью go 1.18, используйте команду go version и пакет debug/buildinfo из go 1.18+.

go mod download

Если в файле go.mod основного модуля указана версия go 1.17 или выше, go mod download без аргументов теперь загружает исходный код только для тех модулей, которые явно требуются в файле go.mod основного модуля. (В go 1.17 или более поздней версии в модуле этот набор уже включает все зависимости, необходимые для сборки пакетов и тестов в основном модуле.) Чтобы также загрузить исходный код для транзитивных зависимостей, используйте go mod download all.

go mod vendor

Подкоманда go mod vendor теперь поддерживает флаг -o для установки выходного каталога. (Другие команды go по-прежнему считываются из каталога vendor в корневом каталоге модуля при загрузке пакетов с параметром -mod=vendor, поэтому в основном этот флаг используется для сторонних инструментов, которым необходимо собирать исходный код пакета.)

go mod tidy

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

go work

Команда go теперь поддерживает режим "Workspace" ("Рабочее пространство"). Если файл go.work найден в рабочем каталоге или родительском каталоге, или он указан с помощью переменной среды GOWORK, команда go будет переведена в режим рабочей области (workspace mode). В режиме рабочей области (workspace mode) файл go.work будет использоваться для определения набора основных модулей, используемых в качестве корней для разрешения модулей, вместо использования обычно находящегося файла go.mod для указания одного основного модуля.

go build -asan

Команда go build и связанные с ней команды теперь поддерживают флаг -asan, который позволяет взаимодействовать с кодом C (или C++), скомпилированным с помощью address sanitizer (параметр компилятора C -fsanitize=address).

go test

Команда go теперь поддерживает дополнительные параметры командной строки для новой поддержки фаззинга:

  • go test поддерживает параметры -fuzz, -fuzztime и -fuzzminimizetime.
  • go clean поддерживает параметр -fuzzcache.

//go:build строки

Go 1.17 представил строки //go:build как более читаемый способ записи ограничений сборки вместо строк // +build. Начиная с Go 1.17, gofmt добавляет строки //go:build в соответствие с существующими строками +build и поддерживает их синхронизацию, в то время как go vet диагностирует, когда они не синхронизированы.

Поскольку релиз Go 1.18 знаменует собой прекращение поддержки Go 1.16, все поддерживаемые версии Go теперь понимают строки //go:build. В Go 1.18 go fix теперь удаляет устаревшие строки // +build в модулях, объявляющих go 1.17 или более позднюю версию в своих файлах go.mod.

gofmt

gofmt теперь одновременно читает и форматирует входные файлы с ограничением памяти, пропорциональным GOMAXPROCS. На машине с несколькими ЦП gofmt теперь должен работать значительно быстрее.

Vet


Обновления для дженериков

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

func Print[T ~int|~string](t T) {
    fmt.Printf("%d", t)
}

потому что он сообщит об ошибке формата в неуниверсальном эквиваленте Print[string]:

func PrintString(x string) {
    fmt.Printf("%d", x)
}

Улучшение точности для существующих проверок

Средства проверки cmd/vet - copylock, printf, sortslice, testinggoroutine - и тесты имеют умеренные улучшения точности для обработки дополнительных шаблонов кода. Это может привести к новым сообщениям об ошибках в существующих пакетах. Например, средство проверки printf теперь отслеживает строки форматирования, созданные путем объединения строковых констант. Поэтому vet сообщит об ошибке в:

// fmt.Printf formatting directive %d is being passed to Println.
// директива форматирования fmt.Printf %d передается в Println.
fmt.Println("%d"+` ≡ x (mod 2)`+"\n", x%2)

Runtime

Сборщик мусора теперь включает источники работы сборщика мусора, не связанные с кучей (например, сканирование стека), при определении частоты запуска. В результате накладные расходы сборщика мусора более предсказуемы, когда эти источники значительны. Для большинства приложений эти изменения будут незначительными; однако некоторые приложения Go теперь могут использовать меньше памяти и тратить больше времени на сборку мусора или наоборот, чем раньше. Предполагаемый обходной путь — настроить GOGC там, где это необходимо.

Среда выполнения (runtime) теперь более эффективно возвращает память операционной системе и в результате настроена на более агрессивную работу.

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

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

Компилятор

В Go 1.17 реализован новый способ передачи аргументов и результатов функций с использованием регистров вместо стека в 64-битной архитектуре x86 в некоторых операционных системах. Go 1.18 расширяет список поддерживаемых платформ, включая 64-разрядные ARM (GOARCH=arm64), 64-разрядные PowerPC с обратным порядком байтов и прямым порядком байтов (GOARCH=ppc64, ppc64le), а также 64-разрядную архитектуру x86 (GOARCH=amd64) на все операционные системы. В 64-разрядных системах ARM и 64-разрядных системах PowerPC бенчмаркинг показывает типичное повышение производительности на 10% и более.

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

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

Новая опция компилятора -asan поддерживает новую опцию команды go -asan.

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

Из-за изменений в компиляторе, связанных с поддержкой дженериков, скорость компиляции Go 1.18 может быть примерно на 15% ниже, чем скорость компиляции Go 1.17. На время выполнения скомпилированного кода это не влияет. Планируется улучшить скорость компилятора в будущих релизах.

Линкер (компоновщик)

Линкер выдает гораздо меньше перемещений. В результате большинство кодовых баз компонуются быстрее, требуют меньше памяти для компоновки и генерируют двоичные файлы меньшего размера. Инструменты, обрабатывающие двоичные файлы Go, должны использовать пакет debug/gosym версии 1.18 для прозрачной обработки как старых, так и новых двоичных файлов.

Новая опция компоновщика -asan поддерживает новую опцию команды go -asan.

Начальная загрузка

При сборке релиза Go из исходного кода и не заданном GOROOT_BOOTSTRAP предыдущие версии Go искали цепочку инструментов начальной загрузки Go 1.4 или более поздней версии в каталоге $HOME/go1.4 (%HOMEDRIVE%%HOMEPATH%\go1.4 в Windows). Теперь Go сначала ищет $HOME/go1.17 или $HOME/sdk/go1.17, а затем возвращается к $HOME/go1.4. Предполагается, что Go 1.19 потребует Go 1.17 или более поздней версии для начальной загрузки, и это изменение должно сделать переход более плавным.

Основная библиотека


Новый пакет debug/buildinfo

Новый пакет debug/buildinfo предоставляет доступ к версиям модулей, информации о контроле версий и флагам сборки, встроенным в исполняемые файлы, созданные командой go. Та же информация также доступна через runtime/debug.ReadBuildInfo для текущего исполняемого файла и через go version -m в командной строке.

Новый пакет net/netip

Новый пакет net/netip определяет новый тип IP-адреса, Addr. По сравнению с существующим типом net.IP тип netip.Addr занимает меньше памяти, является неизменяемым и сравнимым, поэтому он поддерживает == и может использоваться в качестве ключа сопоставления.

В дополнение к Addr пакет определяет AddrPort, представляющий IP-адрес и порт, и Prefix, представляющий префикс сети CIDR.

Пакет также определяет несколько функций для создания и проверки этих новых типов: AddrFrom4, AddrFrom16, AddrFromSlice, AddrPortFrom, IPv4Unspecified, IPv6LinkLocalAllNodes, IPv6Unspecified, MustParseAddr, MustParseAddrPort, MustParsePrefix, ParseAddr, ParseAddrPort, ParsePrefix, PrefixFrom.

Пакет net включает новые методы, которые параллельны существующим методам, но возвращают netip.AddrPort вместо более тяжелых типов net.IP или *net.UDPAddr: Resolver.LookupNetIP, UDPConn.ReadFromUDPAddrPort, UDPConn.ReadMsgUDPAddrPort, UDPConn.WriteToUDPAddrPort, UDPConn.WriteMsgUDPAddrPort. Новые методы UDPConn поддерживают ввод-вывод без аллокаций.

Пакет net также теперь включает функции и методы для преобразования между существующими типами TCPAddr/UDPAddr и netip.AddrPort: TCPAddrFromAddrPort, UDPAddrFromAddrPort, TCPAddr.AddrPort, UDPAddr.AddrPort.

TLS 1.0 и 1.1 отключены по умолчанию на стороне клиента

Если Config.MinVersion не задан, теперь по умолчанию используется TLS 1.2 для клиентских подключений. Ожидается, что любой безопасно обновленный сервер будет поддерживать TLS 1.2, а браузеры требуют его с 2020 года. TLS 1.0 и 1.1 по-прежнему поддерживаются, если для параметра Config.MinVersion установлено значение VersionTLS10. Значение по умолчанию на стороне сервера не изменилось в TLS 1.0.

По умолчанию можно временно вернуться к TLS 1.0, установив переменную среды GODEBUG=tls10default=1. Эта опция будет удалена в Go 1.19.

Отклонение сертификатов SHA-1

crypto/x509 теперь будет отклонять сертификаты, подписанные с помощью хеш-функции SHA-1. Это не относится к самозаверяющим корневым сертификатам. Практические атаки на SHA-1 были продемонстрированы с 2017 года, а общедоступные доверенные центры сертификации не выпускали сертификаты SHA-1 с 2015 года.

Это можно временно отменить, установив переменную среды GODEBUG=x509sha1=1. Эта опция будет удалена в Go 1.19.

Мелкие изменения в библиотеке

Как всегда, в библиотеку внесены различные незначительные изменения и обновления, сделанные с учетом обещания совместимости с Go 1.

bufio

Новый метод Writer.AvailableBuffer возвращает пустой буфер с возможно непустой емкостью для использования с API-интерфейсами, подобными append. После добавления буфер может быть предоставлен для последующего вызова Write и возможно позволит избежать копирования.

Методы Reader.Reset и Writer.Reset теперь используют размер буфера по умолчанию при вызове объектов с нулевым буфером.

bytes

Новая функция Cut нарезает []byte вокруг разделителя. Она может заменить и упростить многие распространенные способы использования Index, IndexByte, IndexRune и SplitN.

Trim, TrimLeft и TrimRight теперь не требуют выделения памяти и, особенно для небольших фрагментов ASCII, работают до 10 раз быстрее.

Функция Title устарела. Она не обрабатывает пунктуацию Unicode и правила использования заглавных букв для конкретного языка и заменяется пакетом golang.org/x/text/cases.

crypto/elliptic

Реализации кривых P224, P384 и P521 теперь поддерживаются кодом, сгенерированным проектами addchain и fiat-crypto, последний из которых основан на формально проверенной модели арифметических операций. Теперь они используют более безопасные полные формулы и внутренние API. Р-224 и Р-384 теперь примерно в четыре раза быстрее. Все конкретные реализации кривых теперь работают с постоянным временем.

Работа с недопустимыми точками кривой (теми, для которых метод IsOnCurve возвращает false и которые никогда не возвращаются Unmarshal или методом Curve, работающим с допустимой точкой) всегда было неопределенным поведением, может привести к атакам восстановления ключа и теперь не поддерживается новым бэкендом. Если для метода P224, P384 или P521 указана недопустимая точка, этот метод теперь будет возвращать случайную точку. Поведение может измениться на явную панику в будущем релизе.

crypto/tls

Новый метод Conn.NetConn позволяет получить доступ к базовому net.Conn.

crypto/x509

Certificate.Verify теперь использует API-интерфейсы платформы для проверки действительности сертификата в macOS и iOS, когда он вызывается с нулевым значением VerifyOpts.Roots или при использовании корневого пула, возвращенного из SystemCertPool.

SystemCertPool теперь доступен в Windows.

В Windows, macOS и iOS, когда в CertPool, возвращенный SystemCertPool, добавлены дополнительные сертификаты, Certificate.Verify выполнит две проверки: одну с использованием API-интерфейсов верификатора платформы и системных корней (roots), а другую с использованием верификатора Go и дополнительных корней (roots). Цепочки, возвращаемые API-интерфейсами верификатора платформы, будут иметь приоритет.

CertPool.Subjects устарел. В Windows, macOS и iOS CertPool, возвращаемый SystemCertPool, будет возвращать пул, который не включает системные корни в срезе, возвращаемом Subjects, поскольку статический список не может должным образом представлять политики платформы и может быть вообще недоступен из API платформы.

Поддержка подписания сертификатов с использованием алгоритмов подписи, зависящих от хэшей MD5 и SHA-1 (MD5WithRSA, SHA1WithRSA и ECDSAWithSHA1), может быть удалена в Go 1.19.

debug/dwarf

Структуры StructField и BasicType теперь имеют поле DataBitOffset, которое содержит значение атрибута DW_AT_data_bit_offset, если он присутствует.

debug/elf

Добавлена константа R_PPC64_RELATIVE.

debug/plan9obj

Метод File.Symbols теперь возвращает новое экспортированное значение ошибки ErrNoSymbols, если в файле нет раздела символов.

embed

Директива go:embed теперь может начинаться с all: для включения файлов, имена которых начинаются с точки или подчеркивания.

go/ast

Согласно предложению Дополнения к go/ast и go/token для поддержки параметризованных функций и типов, в пакет go/ast внесены следующие дополнения:

  • узлы FuncType и TypeSpec имеют новое поле TypeParams для хранения параметров типа, если таковые имеются.
  • Новый узел выражения IndexListExpr представляет выражения индекса с несколькими индексами, используемые для создания экземпляров функций и типов с более чем одним явным аргументом типа.

go/constant

Новый метод Kind.String возвращает удобочитаемое имя для типа получателя.

go/token

Новая константа TILDE представляет токен ~ согласно предложению Дополнения к go/ast и go/token для поддержки параметризованных функций и типов.

go/types

В новом поле Config.GoVersion задается принятая языковая версия Go.

В соответствии с предложением Дополнения к go/types для поддержки параметров типа в пакет go/types внесены следующие дополнения:

  • Новый тип TypeParam, фабричная функция NewTypeParam и связанные методы добавляются для представления параметра типа.
  • Новый тип TypeParamList содержит список параметров типа.
  • Новый тип TypeList содержит список типов.
  • Новая фабричная функция NewSignatureType выделяет сигнатуру с параметрами типа (получателя или функции). Чтобы получить доступ к этим параметрам типа, у типа Signature есть два новых метода Signature.RecvTypeParams и Signature.TypeParams.
  • Named типы имеют четыре новых метода: Named.Origin для получения исходных параметризованных типов экземпляров типов, Named.TypeArgs и Named.TypeParams для получения аргументов типа или параметров типа для экземпляра или параметризованного типа и Named.SetTypeParams для установки типа параметров (например, при импорте именованного типа, когда выделение именованного типа и установка параметров типа не могут выполняться одновременно из-за возможных циклов).
  • Тип Interface имеет четыре новых метода: Interface.IsComparable и Interface.IsMethodSet для запроса свойств набора типов, определенного интерфейсом, и Interface.MarkImplicit и Interface.IsImplicit для установки и проверки того, является ли интерфейс неявным интерфейсом вокруг литерала ограничения типа.
  • Добавлены новые типы Union и Term, фабричные функции NewUnion и NewTerm и связанные методы для представления наборов типов в интерфейсах.
  • Новая функция Instantiate создает экземпляр параметризованного типа.
  • Новая карта Info.Instances записывает функции и экземпляры типов через новый тип Instance.
  • Добавлен новый тип ArgumentError и связанные с ним методы для представления ошибки, связанной с аргументом типа.
  • Новый контекст типа и фабричная функция NewContext добавляются для облегчения совместного использования экземпляров идентичных типов в пакетах с проверкой типов через новое поле Config.Context.

Предикаты AssignableTo, ConvertibleTo, Implements, Identical, IdenticalIgnoreTags и AssertableTo теперь также работают с аргументами, которые являются обобщенными интерфейсами или содержат их, то есть интерфейсы, которые можно использовать только в качестве ограничений типа в коде Go. Обратите внимание, что поведение AssignableTo, ConvertibleTo, Implements и AssertableTo не определено с аргументами, которые являются неэкземплярными универсальными типами, а AssertableTo не определено, если первый аргумент является обобщенным интерфейсом.

html/template

В range пайплайне новая команда {{break}} завершает цикл досрочно, а новая команда {{continue}} немедленно запускает следующую итерацию цикла.

Функция and больше не всегда оценивает все аргументы; она прекращает оценку аргументов после первого аргумента, который оценивается как false. Точно так же функция or теперь прекращает оценку аргументов после первого аргумента, который оценивается как true. Это имеет значение, если какой-либо из аргументов является вызовом функции.

image/draw

Резервные реализации Draw и DrawMask (используемые, когда аргументы не являются наиболее распространенными типами изображений) теперь работают быстрее, когда эти аргументы реализуют необязательные интерфейсы draw.RGBA64Image и image.RGBA64Image, которые были добавлены в Go 1.17.

net

net.Error.Temporary устарела.

net/http

В целях WebAssembly поля методов Dial, DialContext, DialTLS и DialTLSContext в Transport теперь будут правильно использоваться, если они указаны, для выполнения HTTP-запросов.

Новый метод Cookie.Valid сообщает, действителен ли файл cookie.

Новая функция MaxBytesHandler создает обработчик, который оборачивает свои ResponseWriter и Request.Body в MaxBytesReader.

При поиске доменного имени, содержащего символы, отличные от ASCII, преобразование Unicode в ASCII теперь выполняется в соответствии с непереходной обработкой, как определено в стандарте Unicode IDNA Compatibility Processing (UTS #46). Изменена интерпретация четырех различных рун: ß, ς, соединения нулевой ширины U+200D и соединения нулевой ширины U+200C. Непереходная обработка совместима с большинством приложений и веб-браузеров.

os/user

User.GroupIds теперь использует нативную реализацию Go, когда cgo недоступен.

reflect

Новые методы Value.SetIterKey и Value.SetIterValue устанавливают значение, используя итератор карты в качестве источника. Они эквивалентны Value.Set(iter.Key()) и Value.Set(iter.Value()), но выполняют меньше аллокаций.

Новый метод Value.UnsafePointer возвращает значение Value как unsafe.Pointer. Это позволяет вызывающим объектам переходить от Value.UnsafeAddr и Value.Pointer, чтобы исключить необходимость выполнения преобразований uintptr в unsafe.Pointer на месте вызова (как того требуют правила unsafe.Pointer).

Новый метод MapIter.Reset изменяет свой приемник для перебора другой карты. Использование MapIter.Reset позволяет выполнять итерацию без распределения по многим картам.

В Value добавлен ряд методов ( Value.CanInt, Value.CanUint, Value.CanFloat, Value.CanComplex ) для проверки безопасности преобразования.

Value.FieldByIndexErr был добавлен, чтобы избежать паники, возникающей в Value.FieldByIndex при переходе через нулевой указатель к встроенной структуре.

Reflect.Ptr и Reflect.PtrTo были переименованы в Reflect.Pointer и Reflect.PointerTo соответственно для обеспечения согласованности с остальной частью пакета Reflect. Старые имена продолжат работать, но в будущем релизе Go они будут объявлены устаревшими.

regexp

regexp теперь обрабатывает каждый недопустимый байт строки UTF-8 как U+FFFD.

runtime/debug

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

  • GoVersion содержит версию Go, используемую для сборки двоичного файла.
  • Settings — это срез структур BuildSettings, содержащий пары ключ/значение, описывающие сборку.

runtime/pprof

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

strconv

strconv.Unquote теперь отклоняет суррогатные половинки Unicode.

strings

Новая функция Cut разрезает строку вокруг разделителя. Он может заменить и упростить многие распространенные способы использования Index, IndexByte, IndexRune и SplitN.

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

Trim, TrimLeft и TrimRight теперь не требуют выделения памяти и, особенно для небольших фрагментов ASCII, работают до 10 раз быстрее.

Функция Title устарела. Она не обрабатывает пунктуацию Unicode и правила использования заглавных букв для конкретного языка и заменяется пакетом golang.org/x/text/cases.

sync

Новые методы Mutex.TryLock, RWMutex.TryLock и RWMutex.TryRLock получат блокировку, если она в данный момент не удерживается.

syscall

Для Windows была введена новая функция SyscallN, позволяющая выполнять вызовы с произвольным количеством аргументов. В результате Syscall, Syscall6, Syscall9, Syscall12, Syscall15 и Syscall18 устарели в пользу SyscallN.

SysProcAttr.Pdeathsig теперь поддерживается во FreeBSD.

syscall/js

Интерфейс Wrapper был удален.

testing

Увеличен приоритет / в аргументе для -run и -bench. A/B|C/D раньше рассматривались как A/(B|C)/D, а теперь рассматриваются как (A/B)|(C/D).

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

Новый тип testing.F используется новой поддержкой фаззинга. Тесты также теперь поддерживают параметры командной строки -test.fuzz, -test.fuzztime и -test.fuzzminimizetime.

text/template

В range пайплайне новая команда {{break}} завершает цикл досрочно, а новая команда {{continue}} немедленно запускает следующую итерацию цикла.

Функция and больше не всегда оценивает все аргументы; она прекращает оценку аргументов после первого аргумента, который оценивается как false. Точно так же функция or теперь прекращает оценку аргументов после первого аргумента, который оценивается как true. Это имеет значение, если какой-либо из аргументов является вызовом функции.

text/template/parse

Пакет поддерживает новую команду text/template и html/template {{break}} с помощью новой константы NodeBreak и нового типа BreakNode, а также поддерживает новую команду {{continue}} с помощью новой константы NodeContinue и нового типа ContinueNode.

unicode/utf8

Новая функция AppendRune добавляет руну в кодировке UTF-8 к []byte.


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


вторник, 12 мая 2020 г.

Дженерики в Golang (альтернативы и обходные пути)

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

Вот некоторые техники, которые можно использовать вместо параметрического полиморфизма в Go.

Найдите подходящий интерфейс

Опишите общее поведение ваших данных с помощью интерфейса.

Хороший пример - интерфейс io.Reader, который представляет конец потока данных для чтения:

  • многие функции принимают io.Reader в качестве входных данных,
  • и многие типы данных, включая файлы, сетевые подключения и шифры, реализуют этот интерфейс.

Используйте несколько функций

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

Например, два пакета strings и bytes имеют практически одинаковый набор функций.

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

Используйте пустой интерфейс

Если о данных мало что известно, рассмотрите возможность использования пустого интерфейса interface{} в сочетании с утверждениями типа и, возможно, также отражением. Такие библиотеки, как fmt и encoding/json, не могли быть написаны другим способом.


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


пятница, 27 сентября 2019 г.

Дженерики в Golang

Эта статья о том, что будет означать добавление дженериков в Go, и почему это следует сделать. Также будет затронута тема возможного дизайна для добавления дженериков в Go.

Go был выпущен 10 ноября 2009 года. Менее чем через 24 часа появился первый комментарий о дженериках. (В этом комментарии также упоминаются исключения, которые были добавлены к языку, в форме panic и recover в начале 2010 года.)

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

Почему дженерики?

Но что значит добавить дженерики, и зачем это нужно?

Перефразируя Jazayeri: Универсальное программирование позволяет представлять функции и структуры данных в универсальной форме с факторизацией типов.

Что это означает?

Для простого примера, предположим, что мы хотим поменять порядок элементов в срезе (slice) на обратный. Это не то, что нужно делать многим программам, но это не так уж необычно.

Допустим, это срез int.

func ReverseInts(s []int) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

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

Теперь возьмем срез строк.

func ReverseStrings(s []string) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

Если вы сравните ReverseInts и ReverseStrings, вы увидите, что эти две функции абсолютно одинаковы, за исключением типа параметра.

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

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

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

Большинство других статически типизированных языков, таких как C++ или Java или Rust или Swift, поддерживают обобщенные типы (дженерики) для решения именно такого рода проблем.

Обобщенное программирование в Go сегодня

Так как же люди пишут такой код на Go?

В Go вы можете написать одну функцию, которая работает для разных типов срезов, используя тип интерфейса и определяя метод для типов срезов, которые вы хотите передать. Именно так работает функция sort.Sort стандартной библиотеки.

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

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

Другим способом использования интерфейсов для обобщений, который может обойти необходимость написания методов самостоятельно, было бы использование методов определения языка для некоторых типов типов. Это не то, что язык поддерживает сегодня, но, например, язык может определить, что у каждого типа среза есть метод Index, который возвращает элемент. Но чтобы использовать этот метод на практике, он должен будет возвращать пустой тип интерфейса, и тогда теряются все преимущества статической типизации. То есть, не было бы никакого способа определить универсальную функцию, которая берет два различных среза с одним и тем же типом элемента, или которая берет карту (map) одного типа элемента и возвращает срез того же типа элемента. Go - статически типизированный язык, потому что это облегчает написание больших программ; нет желания терять преимущества статической типизации для получения преимуществ дженериков.

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

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

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

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

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

Что дженерики могут принести в Go

Первая и самая важная вещь, которая ожидается от дженериков в Go, - это возможность писать такие функции, как Reverse, не заботясь о типе элемента среза. Мы хотим выделить этот тип элемента. Затем мы можем написать функцию один раз, написать тесты один раз, поместить их в пакет и вызывать их, когда захотим.

Более того, поскольку это мир с открытым исходным кодом, кто-то другой может написать Reverse один раз, и мы можем использовать их реализацию.

На данный момент следует сказать, что "дженерики" могут означать много разных вещей. В частности, они означают шаблоны, встречающиеся в языке C++, которые поддерживают немного больше, чем то, что здесь написано.

Здесь подробно рассмотрен Reverse, но есть много других функций, которые можно написать в общем виде, такие как:

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

Эти примеры доступны на большинстве других языков. Фактически это список, из стандартной библиотеки шаблонов C++.

Есть также примеры, которые характерны для Go с его сильной поддержкой конкурентности.

  • Читать из канала с тайм-аутом
  • Объединить два канала в один канал
  • Вызывать список функций параллельно, возвращая часть результатов
  • Вызывать список функций, используя Context, вернуть результат первой функции для завершения, отменить и очистить лишние go-процедуры

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

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

Также, как упомянуто выше, это не просто функции. Это также структуры данных.

Go имеет две универсальные структуры данных общего назначения, встроенные в язык: срезы (slice) и карты (map). Срезы и карты могут содержать значения любого типа данных, при этом статический тип проверяет сохраненные и извлеченные значения. Значения хранятся как сами по себе, а не как типы интерфейсов. То есть, когда есть []int, срез хранит целые числа напрямую, а не целые числа, преобразованные в тип интерфейса.

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

  • Наборы (Sets)
  • Самобалансирующиеся деревья, с эффективной вставкой и обходом в отсортированном порядке
  • Мультикарты с несколькими экземплярами ключа
  • Конкурентные хэш-карты, поддерживающие конкурентные вставки и поиск без единой блокировки

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

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

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

Вот что Go может извлечь из дженериков. Дженерики могут дать мощные строительные блоки, которые позволят легче делиться кодом и создавать программы.

Преимущества и затраты

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

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

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

* Минимизировать новые концепции

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

* Сложность ложится на автора общего кода, а не на пользователя

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

* Писатель и пользователь могут работать независимо друг от друга

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

* Короткое время сборки, быстрое время выполнения

Естественно, насколько это возможно, необходимо сохранить короткие сроки сборки и быстрое время выполнения, которые Go дает нам сегодня. Дженерики имеют тенденцию вводить компромисс между быстрой сборкой и быстрым выполнением. Насколько это возможно, желательно и то, и другое.

* Сохраняйте ясность и простоту Go

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

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

Эскизный дизайн дженериков в Go

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

В Gophercon 2019 Robert Griesemer и Ian Lance Taylor опубликовали черновик проекта для добавления дженериков в Go. Здесь будут рассмотрены некоторые основные моменты.

Вот общая функция Reverse в этом дизайне.

func Reverse (type Element) (s []Element) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

Вы заметите, что тело функции точно такое же. Только подпись изменилась.

Тип элемента среза был исключен. Теперь он называется Element и стал тем, что называется параметром типа. Вместо того, чтобы быть частью типа параметра slice, теперь это отдельный дополнительный параметр типа.

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

func ReverseAndPrint(s []int) {
    Reverse(int)(s)
    fmt.Println(s)
}

Этот (int) видим после Reverse в данном примере.

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

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

func ReverseAndPrint(s []int) {
    Reverse(s)
    fmt.Println(s)
}

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

Контракты

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

Функция Reverse может работать со срезами любого типа. Единственное, что она делает со значениями типа Element, это присваивание, которое работает с любым типом в Go. Для такого рода универсальной функции, которая является очень распространенным случаем, не нужно говорить ничего особенного о параметре типа.

Рассмотрим другую функцию.

func IndexByte (type T Sequence) (s T, b byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == b {
            return i
        }
    }
    return -1
}

В настоящее время и пакет bytes, и пакет strings в стандартной библиотеке имеют функцию IndexByte. Эта функция возвращает индекс b в последовательности s, где s является либо string, либо []byte. Можно было бы использовать эту единую универсальную функцию для замены двух функций в пакетах bytes и strings. На практике можно не беспокоиться об этом, но это полезный простой пример.

Здесь нужно знать, что параметр типа T действует как string или []byte. Мы можем вызвать len, и мы можем индексировать его, и мы можем сравнить результат операции index с байтовым значением.

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

Вот как контракт Sequence определяется для этого примера.

contract Sequence(T) {
    T string, []byte
}

Это довольно просто, поскольку это простой пример: параметр типа T может быть либо string, либо []byte. Здесь контракт может быть новым ключевым словом или специальным идентификатором, распознаваемым в области действия пакета.

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

Контракты с методами

Вот еще один простой пример функции, которая использует метод String для возврата []string строкового представления всех элементов в s.

func ToStrings (type E Stringer) (s []E) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = v.String()
    }
    return r
}

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

Эта функция требует, чтобы тип элемента реализовал метод String. Контракт Stringer гарантирует это.

contract Stringer(T) {
    T String() string
}

Контракт просто говорит, что T должен реализовать метод String.

Вы можете заметить, что этот контракт выглядит как интерфейс fmt.Stringer, поэтому стоит указать, что аргумент функции ToStrings не является срезом fmt.Stringer. Это срез некоторого типа элемента, где тип элемента реализует fmt.Stringer. Представление в памяти среза типа элемента и среза fmt.Stringer обычно различается, и Go не поддерживает прямые преобразования между ними. Так что это стоит написать, хотя fmt.Stringer существует.

Контракты с несколькими типами

Вот пример контракта с несколькими параметрами типа.

type Graph (type Node, Edge G) struct { ... }

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

func New (type Node, Edge G) (nodes []Node) *Graph(Node, Edge) {
    ...
}

func (g *Graph(Node, Edge)) ShortestPath(from, to Node) []Edge {
    ...
}

Здесь описывается граф, построенный из узлов и ребер. Не требуется конкретной структуры данных для графа. Вместо этого мы говорим, что тип Node должен иметь метод Edges, который возвращает список ребер, которые соединяются с Node. И тип Edge должен иметь метод Nodes, который возвращает два Node, которые соединяет Edge.

Реализация здесь пропущена, но она показывает сигнатуру функции New, которая возвращает Graph, и сигнатуру метода ShortestPath в Graph.

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

Упорядоченные (Ordered) типы

Одна удивительно распространенная жалоба на Go состоит в том, что у него нет функции Min. Или функции Max. Функции Min нет потому, что полезная функция Min должна работать для любого упорядоченного типа, что означает, что она должна быть универсальной.

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

func Min (type T Ordered) (a, b T) T {
    if a < b {
        return a
    }
    return b
}

Ordered контракт говорит, что тип T должен быть упорядоченным типом, что означает, что он поддерживает операторы типа меньше, больше, и так далее.

contract Ordered(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

Ordered контракт - это просто список всех упорядоченных типов, определенных языком. Этот контракт принимает любой из перечисленных типов или любой именованный тип, базовый тип которого является одним из этих типов. В основном, любой тип, который вы можете использовать с оператором <.

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

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

На практике этот контракт, вероятно, попадет в стандартную библиотеку. И поэтому действительно функция Min (которая, вероятно, также будет где-то в стандартной библиотеке) будет выглядеть следующим образом. Здесь идет просто ссылка на Ordered контракт, определенный в пакете contracts.

func Min (type T contracts.Ordered) (a, b T) T {
    if a < b {
        return a
    }
    return b
}

Общие структуры данных

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

type Tree (type E) struct {
    root    *node(E)
    compare func(E, E) int
}

type node (type E) struct {
    val         E
    left, right *node(E)
}

Вот как можно создать новое бинарное дерево. Функция сравнения передается функции New.

func New (type E) (cmp func(E, E) int) *Tree(E) {
    return &Tree(E){compare: cmp}
}

Неэкспортированный метод возвращает указатель либо на слот, содержащий v, либо на место в дереве, куда он должен идти.

func (t *Tree(E)) find(v E) **node(E) {
    pn := &t.root
    for *pn != nil {
        switch cmp := t.compare(v, (*pn).val); {
        case cmp < 0:
            pn = &(*pn).left
        case cmp > 0:
            pn = &(*pn).right
        default:
            return pn
        }
    }
    return pn
}

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

Вот код для проверки, содержит ли дерево значение.

func (t *Tree(E)) Contains(v E) bool {
    return *t.find(e) != nil
}

Вот код для вставки нового значения.

func (t *Tree(E)) Insert(v E) bool {
    pn := t.find(v)
    if *pn != nil {
        return false
    }
    *pn = &node(E){val: v}
    return true
}

Обратите внимание на аргумент типа E для типа node. Вот как выглядит общая структура данных. Как вы можете видеть, это похоже на написание обычного кода Go, за исключением того, что некоторые аргументы типа разбросаны здесь и там.

Использовать дерево довольно просто.

var intTree = tree.New(func(a, b int) int { return a - b })

func InsertAndCheck(v int) {
    intTree.Insert(v)
    if !intTree.Contains(v) {
        log.Fatalf("%d not found after insertion", v)
    }
}

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


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