Страница 1 из 2
Go: дженерики
Добавлено: 07 мар 2023, 18:24
Olej
Дженерики в Go появились совсем недавно, в версии 1.18, март 2022г. ... причём, в версии 1.18 утверждалось, что они ещё могут претерпеть заметные изменения в реализации.
Дженерики в языке Go
2 июн 2021
Как вы уже наверняка знаете, proposal по дженерикам в Golang принят (официально это называется type parameters) и будет имплементирован в go 1.18. Бета будет доступна уже в конце этого года. А это значит, что пора разобраться, на чём в итоге остановились разработчики языка — ведь черновик type parameters постоянно менялся в течение последних лет.
Нужно ли усложнять язык дженериками?
Вопрос дискуссионный. Мнения разделились.
Как известно, язык Go изначально был заточен под максимальную простоту, и обобщение типов может усложнить читабельность кода. Многие противопоставляют Go языку Java, традиционно наполненному обобщениями различного рода, и дженерики — это как первый шаг в эту сторону.
С другой стороны, если надо написать универсальную библиотеку для каких-то универсальных целей, то придётся использовать interface{} или кодогенерацию, а это тоже в общем-то читабельности и надёжности не добавляет. Также необходимо отметить, что разработчики языка сделали всё возможное, чтобы дженерики выглядели и использовались как можно проще. Намного проще, чем в других языках.
Введение в использование дженериков в Golang
понедельник, 2 мая 2022 г.
Дженерики — это самое большое изменение, которое было внесено в Go с момента первого релиза с открытым исходным кодом. В этом посте вы познакомитесь с новыми функциями языка.
...
Дженерики — это способ написания кода, который не зависит от используемых конкретных типов. Функции и типы теперь могут быть написаны для использования любого набора типов.
В документации GoLang:
Tutorial: Getting started with generics
Go: дженерики
Добавлено: 07 мар 2023, 18:26
Olej
Olej писал(а): ↑07 мар 2023, 18:24
Дженерики в Go появились
С чего начинаем?
Конечно же - с проверки используемой версии Go
:
Код: Выделить всё
olej@R420:~$ go version
go version go1.20rc2 linux/amd64
Go: дженерики
Добавлено: 07 мар 2023, 19:01
Olej
Olej писал(а): ↑07 мар 2023, 18:26
С чего начинаем?
Синтаксически это выглядит так - простейший пример:
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go fmt print_slice.go
print_slice.go
Программа, распечатающая слайс (срез массива) переменных любого типа:
Код: Выделить всё
package main
import "fmt"
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
//fmt.Printf("площадь = %.2f\n", многоугольник.square())
}
fmt.Println()
}
func main() {
срез_str := []string{"Hello", "world"}
PrintSlice(срез_str)
срез_int8 := []int8{9, 8, 7, 6, 5, 4}
PrintSlice(срез_int8)
срез_float32 := []float32{9.0, 8.1, 7.2, 6.3, 5.4, 4.5}
PrintSlice(срез_float32)
срез_complex128 := []complex128{9 + 0i, 8 + 1i, 7 + 2i, 6 + 3i}
PrintSlice(срез_complex128)
}
Вообще то,
правильно полностью вызов PrintSlice следовало бы записывать так: PrintSlice[string](срез_str) ... но во многих случаях компилятор может сам сделать
вывод типа из переданных аргументов.
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go build print_slice.go
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ./print_slice
Hello world
9 8 7 6 5 4
9 8.1 7.2 6.3 5.4 4.5
(9+0i) (8+1i) (7+2i) (6+3i)
Go: дженерики
Добавлено: 11 мар 2023, 14:49
Olej
Olej писал(а): ↑07 мар 2023, 19:01
Вообще то, правильно полностью вызов PrintSlice следовало бы записывать так: PrintSlice[string](срез_str) ... но во многих случаях компилятор может сам сделать вывод типа из переданных аргументов.
Синтексически полный вызоов параметризированных (дженерик) функций должен бы записываться так:
Код: Выделить всё
package main
import "fmt"
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
//fmt.Printf("площадь = %.2f\n", многоугольник.square())
}
fmt.Println()
}
func main() {
срез_str := []string{"Hello", "world"}
PrintSlice[string](срез_str)
срез_int8 := []int8{9, 8, 7, 6, 5, 4}
PrintSlice[int8](срез_int8)
срез_float32 := []float32{9.0, 8.1, 7.2, 6.3, 5.4, 4.5}
PrintSlice[float32](срез_float32)
срез_complex128 := []complex128{9 + 0i, 8 + 1i, 7 + 2i, 6 + 3i}
PrintSlice[complex128](срез_complex128)
}
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go build -o print_slice print_slice.0.go
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ls -l print_slice
-rwxrwxr-x 1 olej olej 1858687 мар 11 13:05 print_slice
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ./print_slice
Hello world
9 8 7 6 5 4
9 8.1 7.2 6.3 5.4 4.5
(9+0i) (8+1i) (7+2i) (6+3i)
Но тут на помощь (упрощение) приходит
выведение типов, как показано раньше. Это сильно напоминает то, как делается выведение типов в C++ в стандартах C++11/C++14 (что особенно хорошо видно на примерах касающихся контейнеров STL).
Но выведение типов возможно и работает
не всегда. Тут очень интересно, что на этот счёт пишут сами авторы разработки GoLang -
Дженерики в Go — подробности из блога разработчиков:
29.03.22 15:43
...
Механизм выведения типа сложен, но применять его просто: выведение типа либо происходит, либо нет. Если тип выводится, типы-аргументы можно опустить — тогда вызов параметризованных функций ничем не отличается от вызова обычных функций. Если выведение типа не происходит, в компиляторе выдаётся сообщение об ошибке — тогда мы можем просто указать необходимые типы-аргументы.
Go: дженерики
Добавлено: 12 мар 2023, 01:00
Olej
Olej писал(а): ↑11 мар 2023, 14:49
То, что во всех примерах выше указывалось в скобках […] за обозначением типа T — это ограничения типа (constraints, констрейнты), условия которым должен удовлетворять обобщённый тип T. Это ограничение типа может быть как любым привычным интерфейсом Go, а может быть интерфейсом, перечисляющим полный список типов, для которых интерфейс может быть использован:
Код: Выделить всё
type MyConstraint interface {
int | int8 | int16 | int32 | int64
}
Следующий пример:
Код: Выделить всё
Смотрим следующий пример: поиск присутствия элемента в срезе (массиве)
exist.go :
package main
import "fmt"
func existsInSlice[T comparable](val T, values []T) bool {
for _, v := range values {
if val == v {
return true
}
}
return false
}
func showIn[T comparable](val T, values []T) {
fmt.Printf("%v in %v => %v\n", val, values, existsInSlice(val, values))
}
func main() {
showIn("worlds", []string{"Hello", "world"})
showIn(7, []int8{9, 8, 7, 6, 5, 4})
showIn(8.3, []float32{9.0, 8.1, 7.2, 6.3, 5.4, 4.5})
showIn(7 + 2i, []complex128{9 + 0i, 8 + 1i, 7 + 2i, 6 + 3i})
}
Код: Выделить всё
$ ./exist
worlds in [Hello world] => false
7 in [9 8 7 6 5 4] => true
8.3 in [9 8.1 7.2 6.3 5.4 4.5] => false
(7+2i) in [(9+0i) (8+1i) (7+2i) (6+3i)] => true
Здесь в качестве ограничения типа указан встроенный интерфейс Go comparable — ограничивающий типы, для которых определены операторы сравнения на равенство и неравенство.
Go: дженерики
Добавлено: 12 мар 2023, 01:12
Olej
Следующий пример: поиск большего из 2-х значений
любого типа (файл max.go ).
Код: Выделить всё
package main
import "fmt"
import "golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a T, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max("Hello", "world"))
fmt.Println(Max(9, 4))
fmt.Println(Max(4.5, 5.4))
// fmt.Println(Max(8 + 1i, 7 + 2i))
}
Здесь ограничение типа (констрейнт) импортируется из соответствующего пакета constraints, содержащего достаточно много частных констрейнтов на разные случаи.
Но тут не всё так просто...
Go: дженерики
Добавлено: 12 мар 2023, 01:47
Olej
Olej писал(а): ↑12 мар 2023, 01:12
Но тут не всё так просто...
Если мы не создадим
описание модуля и не осуществим импорт (загрузку) пакета constraints из внешнего сетевого репозитория, как это пошагово описано здесь:
Go: модули - мы будем упорно ловить терминальную ошибку компиляции.
А вот когда мы проделаем
всё там описанное, то:
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go fmt max.go
max.go
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go build max.go
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ls -l max
-rwxrwxr-x 1 olej olej 1839159 мар 11 16:17 max
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ./max
world
9
5.4
Мы получили готовое приложение.
Go: дженерики
Добавлено: 12 мар 2023, 01:50
Olej
Olej писал(а): ↑12 мар 2023, 01:47
Мы получили готовое приложение.
А вот если мы раскомментируем 4-ю строку вызовов, то получим:
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go build max.go
# command-line-arguments
./max.go:17:17: complex128 does not implement constraints.Ordered (complex128 missing in ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string)
Неожиданно?
А эта ошибка напоминает нам о том, что для
комплексных числовых значений не определены соотношения больше-меньше (эти операции имеют смысл, например, для модулей комплексных векторов, но не самих значений).
И это убедительно показывает как работает механизм ограничения типа в дженериках.
Go: дженерики
Добавлено: 12 мар 2023, 01:54
Olej
Olej писал(а): ↑12 мар 2023, 01:12
Здесь ограничение типа (констрейнт) импортируется из соответствующего пакета constraints, содержащего достаточно много частных констрейнтов на разные случаи.
В завершение хорошо бы посмотреть те конкретные конкрейты (категории типов), которые определены в пакете constraints:
Код: Выделить всё
olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ tree `go env GOPATH`/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/constraints
/home/olej/go/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/constraints
├── constraints.go
└── constraints_test.go
0 directories, 2 files
Вот они:
Код: Выделить всё
olej@R420:~/go/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/constraints$ grep ^type `go env GOPATH`/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/constraints/constraints.go
type Signed interface {
type Unsigned interface {
type Integer interface {
type Float interface {
type Complex interface {
type Ordered interface {
Дополнительных объяснений тут не надо.
Go: дженерики
Добавлено: 12 мар 2023, 01:57
Olej
Ну и наконец...
Параметризация типов может использоваться не только с функциями (и, возможно, методами), но также и с ново образуемыми типами:
Код: Выделить всё
// новый тип
type Tree[T interface{}] struct {
left, right *Tree[T]
value T
}
// метод этого же типа
func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }