Дженерики в go

от автора

Привет, Хабр!

Дженерики (или generics) существуют во многих языках, таких как Java, C#, и Rust, но для Go это относительно новая фича, введенная в версии 1.18.

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

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

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

Кратко пройдемся по базе:

Типы как параметры

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

func First[T any](s []T) T {     return s[0] }

T — это type parameter. Он может быть чем угодно: int, string, вашим пользовательским типом, чем угодно. А any — это ограничение, которое в данном случае может быть абсолютно любым типом

Type set

Type Sets (в Go версиях ниже 1.18 назывались Contracts) — это способ описания ограничений на типы, которые могут быть использованы с дженериками.

Допустим, вы пишете универсальную функцию, которая должна работать только с числами. Без Contracts вы были бы вынуждены полагаться на добросовестность других разработчиков (и мы знаем, как это бывает ненадежно). Но с Contracts вы можете явно указать, что ваша функция работает только с типами, которые поддерживают арифметические операции:

type Number interface {     int | float64 // только целые и вещественные числа. }  func Sum[T Number](a, b T) T {     return a + b }

Type inference

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

package main  import "fmt"  // Generic функция, которая работает с любым типом. func Print[T any](s T) {     fmt.Println(s) }  func main() {     // Go выводит тип параметра T как int.     Print(123)      // Go выводит тип параметра T как string.     Print("Hello, Generics!") }

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

package main  import "fmt"  func Merge[T any, U any](first T, second U) {     fmt.Println(first, second) }  func main() {     // явное указание типов необходимо, так как Go не может однозначно вывести их     Merge[int, string](42, "The answer is") }

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

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

Дженерики в Go

Constraints и type sets

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

Допустим, вы хотите создать функцию, которая работает с числами. Без дженериков, вам пришлось бы писать отдельные функции для int, float64, и так далее. Но с дженериками и constraints, вы можете сделать это в одном маху. Вот как:

package main  import "fmt"  // Определяем наш собственный constraint. type Number interface {     int | float64 // Может быть int или float64. }  // UniversalAdd принимает два параметра любого типа, определенного в Number. func UniversalAdd[T Number](a, b T) T {     return a + b }  func main() {     // Работает с int.     fmt.Println(UniversalAdd[int](1, 2))      // Работает с float64.     fmt.Println(UniversalAdd[float64](1.5, 2.3)) }

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

Вам не нужно писать отдельные функции для каждого типа данных.

comparable, any

comparable — это специальный интерфейс, который говорит нам, что типы данных могут быть сравнены с помощью операторов == и !=:

package main  import "fmt"  // Distinct позволяет нам убедиться, что все эементы в слайсе уникальны func Distinct[T comparable](list []T) []T {     unique := make(map[T]bool)     var result []T     for _, item := range list {         if !unique[item] {             unique[item] = true             result = append(result, item)         }     }     return result }  func main() {     // Работает с любым сравнимым типом     fmt.Println(Distinct([]int{1, 2, 2, 3, 4, 4, 4, 5}))     fmt.Println(Distinct([]string{"котик", "кошечка", "кот", "кошка"})) }

Distinct использует comparable для создания универсального метода удаления дубликатов из слайса.

Теперь перейдем к any. Это базовый интерфейс в Go, который, по сути, может быть чем угодно:

package main  import "fmt"  // PrintAny принимает слайс любых элементов и печатает их. func PrintAny(items []any) {     for _, item := range items {         fmt.Println(item)     } }  func main() {     // Можем смешивать разные типы данных!     PrintAny([]any{1, "apple", true, 3.14}) }

PrintAny принимает слайс элементов любого типа и печатает их.

Универсальные функции

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

package main  import "fmt"  // Swap меняет местами значения двух переменных любого типа. func Swap[T any](a, b *T) {     *a, *b = *b, *a }  func main() {     x := 1     y := 2     Swap(&x, &y)     fmt.Println(x, y) // Выведет: 1 2      s1 := "Hello"     s2 := "Habr"     Swap(&s1, &s2)     fmt.Println(s1, s2) // Выведет: Hello Habr }

Swap использует any, что означает, что она может работать с любым типом данных. Это как универсальный ключ к миру переменных!

Создадим что-то более сложное — универсальную структуру кэша:

package main  import "fmt"  // Cache - универсальная структура кэша. // T - тип хранимых значений. type Cache[T any] struct {     store map[string]T }  // NewCache создает новый экземпляр Cache. func NewCache[T any]() *Cache[T] {     return &Cache[T]{store: make(map[string]T)} }  // Set добавляет значение в кэш. func (c *Cache[T]) Set(key string, value T) {     c.store[key] = value }  // Get возвращает значение из кэша. func (c *Cache[T]) Get(key string) (T, bool) {     val, found := c.store[key]     return val, found }  func main() {     // Создаем кэш для int.     intCache := NewCache[int]()     intCache.Set("key1", 10)     fmt.Println(intCache.Get("key1")) // Выведет: 10 true      // Создаем кэш для string.     stringCache := NewCache[string]()     stringCache.Set("hello", "world")     fmt.Println(stringCache.Get("hello")) // Выведет: world true }

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

Применение

Слайсы:

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

package main  import "fmt"  // Filter принимает слайс и функцию-предикат, возвращая новый слайс с элементами, удовлетворяющими условию. func Filter[T any](slice []T, predicate func(T) bool) []T {     var result []T     for _, v := range slice {         if predicate(v) {             result = append(result, v)         }     }     return result }  func main() {     // Фильтруем слайс целых чисел.     ints := []int{1, 2, 3, 4, 5}     even := Filter(ints, func(n int) bool { return n%2 == 0 })     fmt.Println(even) // Выведет: [2 4]      // Фильтруем слайс строк.     strings := []string{"apple", "banana", "cherry"}     withA := Filter(strings, func(s string) bool { return s[0] == 'a' })     fmt.Println(withA) // Выведет: ["apple"] }

Очереди и стеки

Очередь следует принципу FIFO, а стек — LIFO. С дженериками, мы можем создать эти структуры так, чтобы они работали с любыми типами данных:

package main  import "fmt"  // Stack представляет собой универсальный стек. type Stack[T any] struct {     elements []T }  // Push добавляет элемент в стек. func (s *Stack[T]) Push(value T) {     s.elements = append(s.elements, value) }  // Pop удаляет и возвращает верхний элемент стека. func (s *Stack[T]) Pop() (T, bool) {     if len(s.elements) == 0 {         var zero T // Возвращаем нулевое значение для типа T.         return zero, false     }     last := s.elements[len(s.elements)-1]     s.elements = s.elements[:len(s.elements)-1]     return last, true }  func main() {     stack := Stack[int]{}     stack.Push(1)     stack.Push(2)     fmt.Println(stack.Pop()) // Выведет: 2 true     fmt.Println(stack.Pop()) // Выведет: 1 true     fmt.Println(stack.Pop()) // Выведет: 0 false (стек пуст) } 

Деревья

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

package main  import "fmt"  // TreeNode представляет узел в бинарном дереве поиска. type TreeNode[T any] struct {     Value T     Left  *TreeNode[T]     Right *TreeNode[T] }  // Insert добавляет значение в бинарное дерево поиска. func (n *TreeNode[T]) Insert(value T, compare func(a, b T) int) {     if compare(value, n.Value) < 0 {         if n.Left == nil {             n.Left = &TreeNode[T]{Value: value}         } else {             n.Left.Insert(value, compare)         }     } else {         if n.Right == nil {             n.Right = &TreeNode[T]{Value: value}         } else {             n.Right.Insert(value, compare)         }     } }  // InOrder обходит дерево в порядке возрастания. func (n *TreeNode[T]) InOrder(visit func(T)) {     if n == nil {         return     }     n.Left.InOrder(visit)     visit(n.Value)     n.Right.InOrder(visit) }  func main() {     root := &TreeNode[int]{Value: 5}     root.Insert(3, func(a, b int) int { return a - b })     root.Insert(7, func(a, b int) int { return a - b })     root.Insert(1, func(a, b int) int { return a - b })     root.Insert(9, func(a, b int) int { return a - b })      root.InOrder(func(value int) {         fmt.Println(value)     })     // Выведет числа в порядке возрастания: 1, 3, 5, 7, 9 }

Сортировка

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

package main  import "fmt"  // QuickSort сортирует слайс любого сравнимого типа. func QuickSort[T any](data []T, compare func(a, b T) bool) {     if len(data) < 2 {         return     }     left, right := 0, len(data)-1     pivot := len(data) / 2     data[pivot], data[right] = data[right], data[pivot]     for i := range data {         if compare(data[i], data[right]) {             data[left], data[i] = data[i], data[left]             left++         }     }     data[left], data[right] = data[right], data[left]     QuickSort(data[:left], compare)     QuickSort(data[left+1:], compare) }  func main() {     slice := []int{9, 4, 6, 2, 10, 3}     QuickSort(slice, func(a, b int) bool { return a < b })     fmt.Println(slice) // Выведет: [2 3 4 6 9 10] } =

Binary search

package main  import "fmt"  // BinarySearch ищет элемент в отсортированном слайсе и возвращает его индекс. func BinarySearch[T any](data []T, target T, compare func(a, b T) int) int {     low, high := 0, len(data)-1     for low <= high {         mid := (low + high) / 2         comparison := compare(data[mid], target)         if comparison == 0 {             return mid         } else if comparison < 0 {             low = mid + 1         } else {             high = mid - 1         }     }     return -1 }  func main() {     slice := []int{2, 3, 4, 6, 9, 10}     fmt.Println(BinarySearch(slice, 6, func(a, b int) int { return a - b })) // Выведет: 3 } 

Фабрики

С дженериками, мы можем создать универсальные фабрики, способные порождать объекты любого типа:

package main  import "fmt"  // Creator определяет интерфейс для фабрики. type Creator[T any] func() T  // NewInstance создает новый экземпляр типа T. func NewInstance[T any](create Creator[T]) T {     return create() }  // Примеры типов, которые мы можем создавать. type (     Book struct{ Title string }     Game struct{ Name string } )  func main() {     bookCreator := func() Book { return Book{Title: "The Go Programming Language"} }     gameCreator := func() Game { return Game{Name: "Cyberpunk 2077"} }      book := NewInstance(bookCreator)     game := NewInstance(gameCreator)      fmt.Println(book.Title) // Выведет: The Go Programming Language     fmt.Println(game.Name)  // Выведет: Cyberpunk 2077 }

Декораторы

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

package main  import "fmt"  // Decorator оборачивает функцию, добавляя новую функциональность. func Decorator[T any](fn func(T), decorator func(T) T) func(T) {     return func(input T) {         fn(decorator(input))     } }  func main() {     print := func(n int) { fmt.Println("Number:", n) }     double := func(n int) int { return n * 2 }      decorated := Decorator(print, double)     decorated(5) // Выведет: Number: 10 }

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

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

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

Keep coding, keep improving, и до новых встреч на Хабре.

и… с наступающим Новым Годом! ?


ссылка на оригинал статьи https://habr.com/ru/articles/782414/


Комментарии

Добавить комментарий

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