Как и для чего использовать Generic в Go
Generic (Дженерики) в Go — это функция языка, которая позволяет создавать функции, структуры данных и интерфейсы, которые могут работать с разными типами. Другими словами, дженерики позволяют создавать код, не ограниченный конкретным типом или структурой данных. До появления дженериков в Go разработчикам приходилось писать несколько функций для обработки разных типов данных. Этот подход часто был громоздким и приводил к дублированию кода. С помощью дженериков разработчики могут писать более краткий и многократно используемый код, способный обрабатывать различные типы данных.
До появления дженериков в Go разработчикам приходилось писать несколько функций для обработки разных типов данных. Этот подход часто был громоздким и приводил к дублированию кода. С помощью дженериков разработчики могут писать более краткий и многократно используемый код, способный обрабатывать различные типы данных.
Дженерики в Go были представлены в версии 1.18, выпущенной в феврале 2021 года. Реализация дженериков в Go основана на концепции параметров типа. Параметры типа — это заполнители для типов, которые передаются в качестве аргументов функции или структуры данных, что позволяет им работать с различными типами данных.
Что такое Generic types в Go?
Generic types (просто называемые дженериками) — это коды, которые позволяют нам использовать их для различных функций, просто изменяя типы функций. Обобщения были созданы, чтобы сделать код независимым от типов и функций.
Основная цель дженериков — добиться большей гибкости при написании кода с добавлением меньшего количества строк.
Чтобы лучше понять, посмотрите на этот пример ниже. Мы создаем функцию для печати любого параметра типа следующим образом:
func Print(s[] string) { for _, v: = range s { fmt.Print(v) } }
Теперь вместо того, чтобы печатать строку, нам вдруг захотелось напечатать целое число, поэтому мы соответствующим образом изменяем код.
func Print(s[] int) { for _, v: = range s { fmt.Print(v) } }
Но каждый раз менять код может показаться сложной задачей, и именно здесь в игру вступают дженерики. Присвоив любой тип в его общей форме, мы можем использовать один и тот же код для разных функций. Взгляните на это:
func Print[T any](s[] T) { for _, v: = range s { fmt.Print(v) } }
Здесь мы назначили «Т» any
ключевым словом. Этот любой тип позволяет нам анализировать разные типы переменных в одной и той же функции. S — соответствующая переменная, которая является частью T. Теперь, вызвав метод, мы можем напечатать строку и целое число вместе в одной функции.
func main() { str := []string{"Hello", "Again Hello"] intArray := []int {1, 2, 3} Print(str) Print(intArray) }
Как дженерики работают в Go?
Обобщенные функции в Go реализуются с использованием параметров типа, которые позволяют создавать универсальные функции и структуры данных, которые могут работать с разными типами без необходимости явного преобразования типов.
Рассмотрим этот пример, где параметр типа «T» определяется с использованием ключевого слова «any», которое указывает, что с этой функцией можно использовать любой тип.
func Swap[T any](a, b * T) { * a, * b = * b, * a }
Затем тело функции выполняет простую замену значений, на которые указывают два указателя, переданные в качестве аргументов.
Когда функция вызывается, компилятор генерирует конкретную версию функции для типа, который используется с этой функцией. Например, если функция вызывается с двумя указателями на целые числа, компилятор генерирует версию функции, которая работает с целыми числами.
Обобщенные типы в Go также поддерживают ограничения типов, которые позволяют использовать более конкретные типы с универсальными функциями и структурами данных. Ограничения типа указываются с помощью ключевого слова «interface», за которым следует имя интерфейса и методы, которые должен реализовать тип.
Например, следующая универсальная функция определяет ограничение типа, которое требует, чтобы параметр типа был срезом целых чисел:
func Sum[T Slice[Int]](slice T) int { sum: = 0 for _, v: = range slice { sum += v } return sum }
Здесь Slice[Int]
ограничение гарантирует, что с функцией «Сумма» можно использовать только фрагменты целых чисел.
Мы увидели, насколько важны параметры типа и ограничения типа в универсальных шаблонах. В следующих разделах мы более подробно рассмотрим их определение и работу.
Что такое параметры типа?
В Go параметры типа указываются с помощью списка параметров типа, заключенного в квадратные скобки сразу после имени функции, структуры данных или интерфейса. Параметры типа представлены одной заглавной буквой или последовательностью заглавных букв, заключенных в угловые скобки.
Параметры типа используются для создания универсальных функций, структур данных и интерфейсов в Go. Параметры типа — это заполнители для типов, которые определяются во время компиляции.
Например, рассмотрим приведенный выше пример, в котором показано объявление функции, использующее параметр типа. В этой функции параметр типа представлен заглавной буквой «Т».
Ключевое слово «any» указывает, что функция может работать с любым типом. При вызове этой функции параметр типа заменяется фактическим типом аргумента, переданного функции.
Параметры типа позволяют создавать более универсальный и повторно используемый код в Go, позволяя функциям и структурам данных работать с различными типами данных.
Использование параметров типа в дженериках
В примере, который мы видели выше, мы увидели, как включить более одного типа переменных в одну и ту же функцию.
В примере функция объявлена с параметром типа «T» с использованием ключевого слова «any». Ключевое слово «any» указывает, что функция может работать с любым типом. Функция принимает в качестве аргумента фрагмент типа «T» и печатает его содержимое.
Чтобы использовать эту функцию, вы можете вызвать ее со срезом любого типа, как показано ниже:
intSlice: = [] int { 1, 2, 3, 4, 5 } stringSlice: = [] string { "apple", "banana", "cherry" } PrintSlice(intSlice) // prints 1 2 3 4 5 PrintSlice(stringSlice) // prints apple banana cherry
В этом примере PrintSlice
функция вызывается с фрагментом целых чисел и фрагментом строк.
Параметр типа «T» заменяется фактическими типами аргументов, передаваемых функции.
Вы также можете создавать общие структуры данных и интерфейсы в Go, используя параметры типа. Вот пример общей структуры данных — стека, в которой используется параметр типа:
type Stack[T any] struct { items[] T } func(s * Stack[T]) Push(item T) { s.items = append(s.items, item) } func(s * Stack[T]) Pop() T { if len(s.items) == 0 { panic("stack is empty") } item: = s.items[len(s.items) - 1] s.items = s.items[: len(s.items) - 1] return item }
- Здесь структура данных Stack объявляется с параметром типа « T » с использованием ключевого слова « any ».
- Метод Push принимает в качестве аргумента элемент типа « T » и добавляет его в стек.
- Метод Pop возвращает элемент типа « T » с вершины стека.
Чтобы использовать эту структуру данных, вы можете создать стек любого типа:
intStack: = & Stack[int] {} stringStack: = & Stack[string] {} intStack.Push(1) intStack.Push(2) intStack.Push(3) stringStack.Push("apple") stringStack.Push("banana") stringStack.Push("cherry") fmt.Println(intStack.Pop()) // prints 3 fmt.Println(stringStack.Pop()) // prints cherry
В этом примере создаются два стека: один типа int, другой типа string. Параметр типа «T» заменяется фактическими типами создаваемых стеков.
Ограничения типов
Ограничения типов в универсальных шаблонах определяют набор типов, которые можно использовать с универсальной функцией или структурой данных. Ограничения типов позволяют компилятору обеспечить безопасность типов и гарантировать, что с универсальной конструкцией используются только совместимые типы.
Ограничения типа указываются с помощью ключевого слова « interface », за которым следует имя интерфейса и методы, которые тип должен реализовать. Например, рассмотрим следующую универсальную функцию, использующую ограничение типа:
func Max[T comparable](a, b T) T { if a > b { return a } return b }
В этом примере параметр типа « T » ограничен « сравнимым » интерфейсом, который требует, чтобы тип реализовывал операторы сравнения (>, <, >=, <=). Это гарантирует, что функцию можно вызывать только с типами, поддерживающими сравнение.
comparable
— это встроенный интерфейс, который используется для ограничения параметров универсального типа только теми типами, которые поддерживают операторы сравнения ( <
, <=
, >
, >=
и ==
).
Интерфейс comparable
неявно определен спецификацией языка Go и не требует явного определения в коде. Это означает, что любой тип, поддерживающий операторы сравнения, может использоваться в качестве параметра типа функции Max
без какого-либо дополнительного объявления интерфейса comparable
.
Ограничения типов также могут представлять собой определяемые пользователем интерфейсы, которые допускают более конкретные ограничения типов, которые можно использовать с универсальной функцией или структурой данных. Например, рассмотрим следующий пользовательский интерфейс:
type Number interface { Add(other Number) Number Sub(other Number) Number Mul(other Number) Number Div(other Number) Number }
Этот интерфейс определяет набор методов, которые должен реализовать тип, чтобы считаться «Число». Универсальная функция или структура данных, использующая этот интерфейс в качестве ограничения типа, может использоваться только с типами, реализующими эти методы, обеспечивая безопасность типов и совместимость.
Ограничения типов в дженериках в Go позволяют обеспечить безопасность типов и ограничить набор типов, которые можно использовать с универсальной конструкцией, сохраняя при этом гибкость и возможность повторного использования, которые обеспечивают дженерики.
Примеры использования дженериков в Golang
Вот несколько примеров использования дженериков в Go:
1. Generic functions:
Эта функция принимает срез любого типа T и значение типа T и возвращает индекс значения в срезе. Ключевое any
слово в параметре type указывает, что можно использовать любой тип.
func findIndex[T any](slice []T, value T) int { for i, v := range slice { if reflect.DeepEqual(v, value) { return i } } return -1 }
Generic functions в Golang
2. Generic types:
Это определяет общий тип стека, который может содержать элементы любого типа T. any
Ключевое слово указывает, что любой тип может использоваться в качестве типа элемента.
type Stack[T any] []T func (s *Stack[T]) Push(value T) { *s = append(*s, value) } func (s *Stack[T]) Pop() T { if len(*s) == 0 { panic("Stack is empty") } value := (*s)[len(*s)-1] *s = (*s)[:len(*s)-1] return value }
3. Ограничения на параметры типа:
Это определяет ограничение типа для параметра типа T, которое требует реализации интерфейса Equatable
. Это позволяет findIndex
функции использовать Equals
метод для сравнения значений типа T.
type Equatable interface { Equals(other interface{}) bool } func findIndex[T Equatable](slice []T, value T) int { for i, v := range slice { if v.Equals(value) { return i } } return -1 }
Это всего лишь несколько примеров того, как дженерики можно использовать в Go для написания более гибкого и многократно используемого кода.
Ограничения дженериков
Хотя дженерики в Go принесли в язык множество преимуществ и новых возможностей, их реализация все еще имеет некоторые ограничения и проблемы. Вот некоторые из основных ограничений дженериков в Go:
- Производительность. Одна из основных проблем, связанных с дженериками в Go, — это потенциальное влияние на производительность. С появлением дженериков компилятору Go необходимо генерировать код для разных типов во время компиляции, что может привести к увеличению размера двоичных файлов и замедлению времени компиляции.
- Ограничения типов. Реализация дженериков в Go опирается на ограничения типов для обеспечения безопасности типов. Однако эти ограничения могут быть ограничительными и ограничивать типы, которые можно использовать с универсальными функциями и структурами данных.
- Сложность синтаксиса. Синтаксис объявления и использования универсальных функций и структур данных может быть сложным и трудным для понимания, особенно для новичков.
- Сообщения об ошибках. Сообщения об ошибках, генерируемые компилятором Go для проблем, связанных с дженериками, могут быть трудными для понимания, что усложняет отладку и устранение неполадок.
- Читабельность кода: дженерики в Go иногда могут сделать код менее читаемым и трудным для понимания, особенно если широко используются ограничения типов и параметры типов.
- Переключение невозможно. Если вы хотите переключиться с одного базового универсального типа на другой, использование дженериков невозможно. Единственный способ сделать это — использовать интерфейс и запустить функцию переключения типа во время выполнения.
func is64Bit[T Float](v T) T { switch (interface {})(v).(type) { case float32: return false case float64: return true } }
Последние мысли
Дженерики предоставляют мощный, но простой метод создания универсальных интерфейсов, методов для структур и функций.
Они позволяют сократить количество избыточной информации и, по крайней мере в некоторых случаях, представляют собой превосходную альтернативу размышлениям. Конечно, основная причина, по которой на протяжении значительного времени яростно выступали против дженериков, заключалась в том, что они могли усложнить чтение и анализ кода, что, казалось бы, противоречило простоте Go.
С другой стороны, дженерики — отличное и необходимое дополнение к языку, если их использовать разумно и там, где это имеет смысл.