Дженерики в Go с примерами кода (Generics in Golang)

Несколько лет назад для Go были предложены дженерики, и в начале этого года они наконец были приняты в язык.

Как дженерики действительно повлияют на Go? Изменит ли это то, как мы кодируем?

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

Что на самом деле меняют дженерики в Go?

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

Чтобы по-настоящему понять, что это значит, давайте рассмотрим очень простой случай.

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

func Print(s []string) {
	for _, v := range s {
		fmt.Print(v)
	}
}

Просто, верно? Что, если мы хотим, чтобы срез был целым числом? Для этого вам нужно будет создать новый метод:

func Print(s []int) {
	for _, v := range s {
		fmt.Print(v)
	}
}

Эти решения могут показаться излишними, так как мы только меняем параметр. Но до дженериков так мы решали это в Go, не прибегая к обертке в какой-то интерфейс.

А теперь с помощью дженериков мы можем объявлять наши функции следующим образом:

func Print[T any](s []T) {
	for _, v := range s {
		fmt.Print(v)
	}
}

Один метод для любого типа переменной — удобно, не так ли?

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

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

Ограничения дженериков

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

Но пример, который я привел раньше, был очень простым. Print, например, довольно прост, так как Golang может распечатать любой тип переменной.

Что, если мы хотим делать более сложные вещи? Допустим, мы определили собственные методы для структуры и хотим вызвать ее:

package main

import (
	"fmt"
)

type worker string

func (w worker) Work(){
	fmt.Printf("%s is working\n", w)
}


func DoWork[T any](things []T) {
    for _, v := range things {
        v.Work()
    }
}

func main() {
	var a,b,c worker
	a = "A"
	b = "B"
	c = "C"
	DoWork([]worker{a,b,c})	
}

Результат работы программы:

type checking failed for main
prog.go2:25:11: v.Work undefined (type bound for T has no method Work)

Код не работает, потому что срез, обрабатываемый внутри функции, имеет тип any и не реализует метод Work, из-за чего он не запускается.

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

package main

import (
	"fmt"
)

type Person interface {
    Work()
}

type worker string

func (w worker) Work(){
	fmt.Printf("%s is working\n", w)
}

func DoWork[T Person](things []T) {
    for _, v := range things {
        v.Work()
    }
}

func main() {
	var a,b,c worker
	a = "A"
	b = "B"
	c = "C"
	DoWork([]worker{a,b,c})
}

Вывод программы:

A is working
B is working
C is working

Ну, это работает с интерфейсом, но просто наличие интерфейса без дженериков тоже хорошо работает:

package main

import (
	"fmt"
)

type Person interface {
    Work()
}

type worker string

func (w worker) Work(){
	fmt.Printf("%s is working\n", w)
}

func DoWorkInterface(things []Person) {
    for _, v := range things {
        v.Work()
    }
}

func main() {
	var d,e,f worker
	d = "D"
	e = "E"
	f = "F"
	DoWorkInterface([]Person{d,e,f})
}

Вывод:

D is working
E is working
F is working

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

Ранее мы столкнулись типом any.

Теперь давайте рассмотрим comparable.

func Equal[T comparable](a, b T) bool {
    return a == b
}

func main() {
	Equal("a","a")
}

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

package main

import(
	"fmt"
)

type Number interface {
    type int, float64
}

func MultiplyTen[T Number](a T) T{
	return a*10
}

func main() {
	fmt.Println(MultiplyTen(10))
	fmt.Println(MultiplyTen(5.55))
}

Это очень удобно — у нас может быть одна функция для простого математического выражения.

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

Other Ways to Use Genericsclearvolume_up26 / 5 000

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

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

type GenericSlice[T any] []T

Пример:

func (g GenericSlice[T]) Print() {
	for _, v := range g {
		fmt.Println(v)
	}
}

func Print [T any](g GenericSlice[T]) {
	for _, v := range g {
		fmt.Println(v)
	}
}

func main() {
	g := GenericSlice[int]{1,2,3}
	
	g.Print() //1 2 3
	Print(g) //1 2 3
}

Использование варьируется в зависимости от ваших потребностей.

Для лучшего понимая дженериков в GO , предалагю ознакомиться с сайтом https://bitfieldconsulting.com/golang/generics

Ответить