
Общее программирование в Go
2 июня 2022 г.Введение
Общее программирование на Go всегда было неудобным по сравнению с другими компилируемыми языками. Наиболее популярные способы реализации этого в Go — использование интерфейсов, приведение типов и генерация кода. Но каждый метод имеет свои существенные ограничения. Например, использование интерфейсов требует реализации интерфейса для каждого типа данных. Приведение типов приводит к потенциальным ошибкам во время выполнения. А в случае генерации кода приходится писать генераторы, что занимает много времени.
Основная цель статьи — понять, как универсальные методы работают в Go, и сравнить их производительность с предыдущими методами универсального программирования в Go. Я реализую популярную функцию «Map», которая перебирает массив данных и преобразует каждый элемент с помощью функции обратного вызова.
:::Информация
Примеры кода статьи хранятся в репозитории Github.
Общие функции Go
Общие функции были добавлены в Go в версии 1.18.
Общие функции относятся к механизму полиморфизма во время компиляции, в частности к параметрическому полиморфизму. Это функции, определенные с помощью параметров типа, предназначенные для разрешения с помощью информации о типе во время компиляции. Компилятор использует эти типы для создания экземпляров подходящих версий, соответствующим образом разрешая любую перегрузку функции.
Общие функции в Go позволяют:
- определить параметры типа для функций и типов.
- определять типы интерфейсов как наборы типов, включая типы, не имеющие методов.
- определить вывод типа, что позволяет во многих случаях опускать аргументы типа при вызове функции
На практике это дает следующие возможности:
- написание кода становится удобным для разработчиков, поскольку нет необходимости реализовывать или генерировать код для каждого нового типа данных
- безопасность типа времени компиляции
- тип безопасности во время выполнения
Общая функция Go — пример карты
Давайте посмотрим, как общие функции работают в Go. А для реального примера мы будем использовать функцию Map. Функция принимает срез и обратный вызов, который изменяет каждый элемент и возвращает новый срез.
Реализация функции Map для целочисленных значений выглядит следующим образом:
```иди
// Карта изменяет каждый элемент списка и возвращает новый измененный фрагмент.
func MapT any []T {
если список == ноль {
вернуть ноль
если изменить == ноль {
список возврата
сопоставлено: = сделать ([] T, len (список))
для i, элемент := список диапазонов {
mapped[i] = изменить (элемент)
возврат сопоставлен
Есть две проверки значений nil
для списка и функция обратного вызова для безопасности. После этого идет новый слайс такой же длины, как и список. Затем функция выполняет итерацию по списку, изменяет каждый элемент и записывает элемент в новый слайс mapped
. Пример работы функции:
```иди
основной пакет
основная функция () {
// печатает 2, 4, 6
fmt.Println(Map([]int{1,2,3}, func(item int) int {
возврат товара * 2
Однако, если есть необходимость использовать функцию Map с другим типом, необходимо реализовать новую функцию. Эта архитектура не масштабируема и сложна в обслуживании. Но с помощью новых Generic Functions мы можем создать одну функцию, которая будет работать со всеми типами, которые нам нужны:
```иди
// Карта изменяет каждый элемент списка и возвращает новый измененный фрагмент.
func MapV any []V {
если список == ноль {
вернуть ноль
если изменить == ноль {
список возврата
сопоставлено: = сделать ([] V, len (список))
для i, элемент := список диапазонов {
mapped[i] = изменить (элемент)
возврат сопоставлен
:::Информация
Тип any
— это новый псевдоним для interface{}
.
Реализация функции похожа на реализацию целочисленного отображения. Единственная разница заключается в сигнатуре функции. Теперь есть определение параметра типа [V any]
, что означает, что функция может обрабатывать любой тип, но он должен быть того же типа в функции обратного вызова modify func(item V) V) []V
. Давайте посмотрим, как функция Map работает для разных типов:
```иди
основной пакет
импорт (
"ФМТ"
"струны"
введите структуру человека {
строка имени
возраст
основная функция () {
// печатает [2 4 6]
fmt.Println(Map([]int{1, 2, 3}, func(item int) int {
возврат товара * 2
// печатает [ПРИВЕТ, МИР]
fmt.Println(Map([]string{"hello", "world"}, func(item string) string {
вернуть strings.ToUpper (элемент)
// печатает [{Линда 19} {Иоанн 23}]
fmt.Println(Map([]person{{name: "linda", age: 18}, {name: "john", age: 22}}, func(p person) person {
p.name = strings.Title(p.name)
страница += 1
вернуть р
Общие функции Go
В этом разделе исследуется, как универсальные функции ведут себя во время компиляции и выполнения программы.
Безопасность типа во время выполнения
Для исследования я буду использовать следующую программу Go:
```иди
основной пакет
импорт (
"ФМТ"
"время выполнения"
"струны"
введите структуру человека {
строка имени
возраст
основная функция () {
целые: = [] целые {1, 2, 3}
doubledInts := Map(ints, func(item int) int {
возврат товара * 2
// печатает [2 4 6]
fmt.Println (удвоенные числа)
слова: = [] строка {"привет", "мир"}
CapitalizedWords: = Карта (слова, функция (строка элемента) строка {
вернуть strings.ToUpper (элемент)
// печатает [ПРИВЕТ, МИР]
fmt.Println (слова с заглавной буквы)
люди := []человек{{имя: "Линда", возраст: 18}, {имя: "Джон", возраст: 22}}
модифицированные люди := Map(люди, func(p person) person {
p.name = strings.Title(p.name)
страница += 1
вернуть р
// печатает [{Линда 19} {Иоанн 23}]
fmt.Println(модифицированные люди)
время выполнения.Точка останова()
// Карта изменяет каждый элемент списка и возвращает новый измененный фрагмент.
func MapT any []T {
если список == ноль {
вернуть ноль
если изменить == ноль {
список возврата
сопоставлено: = сделать ([] T, len (список))
для i, элемент := список диапазонов {
mapped[i] = изменить (элемент)
время выполнения.Точка останова()
возврат сопоставлен
Есть две точки останова во время выполнения для целей отладки. Для удобства я использую JetBrains Goland IDEA для отладки кода.
После запуска программы в режиме отладки мы видим, что первый вызов функции Map содержит list
с []int
.
Однако следующие два вызова функций имеют разные типы во время выполнения:
Поэтому конкретный, строго определенный тип передается от вызывающей функции во время выполнения внутри функции Map.
Кроме того, в функции main также определены все типы:
Мы можем сделать вывод, что дженерики в Go сохраняют сохраняют информацию о своем типе во время выполнения, и на самом деле Go не знает об универсальном «шаблоне» во время выполнения — только о том, как он был создан.
Чтобы убедиться, что этот тип сохраняется во время компиляции, мы можем попробовать присвоить capitalizedWords []string
для doubleInts []int
:
```javascript
целые: = [] целые {1, 2, 3}
doubledInts := Map(ints, func(item int) int {
возврат товара * 2
слова: = [] строка {"привет", "мир"}
CapitalizedWords: = Карта (слова, функция (строка элемента) строка {
вернуть strings.ToUpper (элемент)
doubleInts = заглавные слова
Здесь происходит ошибка компиляции:
``` ударить
./main.go:24:16: нельзя использовать заглавные слова (переменная типа []string) как тип []int в присваивании
Поэтому Go обеспечивает безопасность типов для универсальных типов во время выполнения.
Создание экземпляра во время выполнения
Давайте посмотрим, что произойдет, если мы попытаемся проверить универсальную функцию во время выполнения, используя отражение:
```иди
основной пакет
импорт (
"ФМТ"
"отражать"
основная функция () {
fmt.Println(reflect.TypeOf(Карта))
// Карта изменяет каждый элемент списка и возвращает новый измененный фрагмент.
func MapT any []T {
если список == ноль {
вернуть ноль
если изменить == ноль {
список возврата
сопоставлено: = сделать ([] T, len (список))
для i, элемент := список диапазонов {
mapped[i] = изменить (элемент)
возврат сопоставлен
Попытка скомпилировать программу возвращает ошибку:
``` ударить
./main.go:9:29: нельзя использовать универсальную функцию Map без создания экземпляра
Следовательно, дженерики бесполезны в Go до тех пор, пока не будут созданы экземпляры. Невозможно ссылаться на универсальные «шаблоны» с помощью отражения, что означает невозможность создания экземпляров новых типов с использованием универсальных шаблонов во время выполнения. Это похоже на то, как если бы общие типы не существовали в скомпилированных двоичных файлах Golang.
Производительность общих функций
Мы исследовали, как работают универсальные функции во время компиляции и выполнения. Последний важный вопрос: как быстро они работают?
Существует три различных реализации функции Map:
```иди
// Карта изменяет каждый элемент списка и возвращает новый измененный фрагмент.
func MapT any []T {
если список == ноль {
вернуть ноль
если изменить == ноль {
список возврата
сопоставлено: = сделать ([] T, len (список))
для i, элемент := список диапазонов {
mapped[i] = изменить (элемент)
возврат сопоставлен
// MapTyped изменяет каждый элемент списка и возвращает новый измененный фрагмент. Работает только с целочисленными значениями.
func MapTyped (список [] int, изменить func (item int) int) [] int {
если список == ноль {
вернуть ноль
если изменить == ноль {
список возврата
сопоставлено: = сделать ([] int, len (список))
для i, элемент := список диапазонов {
mapped[i] = изменить (элемент)
возврат сопоставлен
// MapAny изменяет каждый элемент списка и возвращает новый измененный фрагмент. Он работает с любым типом, поэтому вы должны сами приводить типы.
func MapAny(список []любой, изменить func(элемент любой) любой) []любой {
если список == ноль {
вернуть ноль
если изменить == ноль {
список возврата
сопоставлено: = сделать ([] любой, len (список))
для i, элемент := список диапазонов {
mapped[i] = изменить (элемент)
возврат сопоставлен
Для теста мы используем список целых чисел []int{1,2,3}
и функцию обратного вызова, которая удваивает каждое целое значение:
```иди
func BenchmarkGenericMap(b *testing.B) {
для я := 0; я < б.Н; я++ {
Map([]int{1, 2, 3}, func(item int) int {
возврат товара * 2
func BenchmarkTypedMap(b *testing.B) {
для я := 0; я < б.Н; я++ {
MapTyped([]int{1, 2, 3}, func(item int) int {
возврат товара * 2
func BenchmarkAnyMap(b *testing.B) {
для я := 0; я < б.Н; я++ {
MapAny([]any{1, 2, 3}, func(item any) any {
возвращаемый элемент.(целое число) * 2
После вызова go test -bench=. -benchmem -v ./...
, у нас есть результаты тестов, которые описаны в таблице ниже:
| Тип функции карты | Количество операций | нс/оп | байт/оп | выделяет/опера |
| Общий | 42033705 | 28,90 | 24 | 1 |
| Напечатано | 41317022 | 29.16 | 24 | 1 |
| Любой (с использованием приведения типов) | 17563975 | 68,61 | 48 | 1 |
Основные выводы:
- Универсальная функция имеет ту же производительность, что и реализация функции специального типа.
- в среднем Generic Function превосходит Any (с использованием приведения типов):
- операции выполняются в \~2,4 раза быстрее
- потребляет половину памяти
- улучшения производительности были результатом устранения необходимости использовать приведение типов
Вывод
Мы исследовали, что такое общие функции и как они работают в Go. Кроме того, мы сравнили производительность общих функций с предыдущими методами общего программирования. Наконец, основные выводы:
- Дженерики позволяют написать функцию только один раз и использовать ее для разных типов данных без дополнительного кода. Поэтому его легче масштабировать и поддерживать.
- Универсальные функции Go создаются во время компиляции и не отображаются во время выполнения программы. Это предотвращает неожиданное поведение в программе и гарантирует безопасность типов.
- Универсальные функции Go имеют такую же производительность, как и реализация функций со специальными типами, но они предпочтительнее, поскольку нет необходимости писать новую функцию для каждого типа данных.
Использованная литература
- Общая функция — Википедия (https://en.wikipedia.org/wiki/Generic_function)
- Введение в Generics — язык программирования Go (https://go.dev/blog/intro-generics)
- Используйте дженерики трудным путем (https://github.com/akutz/go-generics-the-hard-way)
Оригинал