Menu

Generyki w Go (Golang): parametry typów, ograniczenia i przykłady

Generyki (Go 1.18+) pozwalają jednej funkcji lub jednemu typowi działać z wieloma typami przy zachowaniu bezpieczeństwa typów. Poznaj parametry typów, ograniczenia any, comparable i cmp.Ordered, własne ograniczenia z ~, typy generyczne i sytuacje, w których lepiej ich nie używać.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Funkcja generyczna

Parametry typów zapisuje się w nawiasach kwadratowych przed zwykłymi parametrami. Każdy z nich ma ograniczenie, które mówi, jakie typy są dozwolone.

T i U są wnioskowane z argumentów, więc rzadko piszesz Map[int, string](...). Inferencja działa na podstawie argumentów funkcji; nie potrafi wywnioskować typu, który występuje tylko w wyniku. Dla func Zero[T any]() T musisz napisać Zero[int]().

Przed Go 1.18 tę funkcję trzeba było pisać osobno dla każdego typu albo przyjmować []interface{} i tracić bezpieczeństwo typów.

Ograniczenia

Ograniczenie (constraint) to interfejs. Określa, jakich typów można użyć, a w zamian mówi kompilatorowi, jakie operacje są dozwolone na wartościach tego typu.

OgraniczenieDopuszczaPozwala użyć
anykażdy typprzypisania, przekazywania, przechowywania
comparabletypy obsługujące ====, !=, kluczy mapy
cmp.Ordered (Go 1.21)liczby całkowite, zmiennoprzecinkowe, stringi<, <=, >, >=, a także ==
interfejs z metodamitypy z tymi metodamiwywoływania tych metod
unia typów, jak ~int | ~float64dokładnie te typyoperatorów obsługiwanych przez wszystkie z nich

Z any zamiast comparable wyrażenie v == target się nie kompiluje: invalid operation: v == target (incomparable types in type set). To ograniczenie sprawia, że operator jest dozwolony.

Te dwie funkcje pomocnicze już istnieją: slices.Index i slices.Max. Standardowe pakiety slices, maps i cmp pokrywają większość codziennych potrzeb generycznych, więc sprawdź je, zanim napiszesz własne.

Własne ograniczenia z uniami typów i ~

Ograniczenie może wymieniać typy przez |. Dozwolone są te operatory, które obsługuje każdy z wymienionych typów.

Tylda ma znaczenie. ~int64 oznacza "dowolny typ, którego typem bazowym jest int64", więc nazwany typ Cents zostaje zaakceptowany. Zapisane jako zwykłe int64, Sum([]Cents{...}) kończy się błędem Cents does not satisfy Number (possibly missing ~ for int64 in Number).

Interfejsów zawierających unie typów można używać tylko jako ograniczeń, nigdy jako zwykłych typów zmiennych. var n Number się nie kompiluje.

Pakiet golang.org/x/exp/constraints ma gotowe ograniczenia Integer, Float i Signed. Jest poza biblioteką standardową, więc tutejsze środowisko uruchomieniowe nie może go zaimportować; standardowym ograniczeniem jest cmp.Ordered.

Typy generyczne

Struktury, slice'y, mapy i inne typy też mogą mieć parametry typów. Metody typu generycznego używają parametrów odbiorcy.

Dwa szczegóły z tego przykładu pojawiają się bez przerwy:

  • Wartość zerowa T. var zero T to sposób na zwrócenie "niczego" z kodu generycznego. Nie ma T{} ani nil, które działałyby dla każdego typu.
  • Instancjonowanie. Typ generyczny trzeba zinstancjonować przed użyciem: Stack[string], a nie Stack. Wewnątrz jego własnych metod odbiorcę zapisuje się jako Stack[T].

Go 1.24 dodało generyczne aliasy typów: type Set[T comparable] = map[T]struct{} teraz się kompiluje.

Ograniczenia, na które trafisz

  • Brak parametrów typów w metodach. func (s *Stack[T]) Map[U any](...) jest niedozwolone. Użyj funkcji na najwyższym poziomie.
  • Brak specjalizacji. Nie da się napisać osobnej implementacji dla T = string. Obejściem jest switch typów na any(v) wewnątrz funkcji, a zwykle jest to znak, że generyki to złe narzędzie.
  • Brak ograniczeń operatorów po nazwie. Nie da się powiedzieć "dowolny typ z metodą +"; wymieniasz typy w unii.
  • Dostęp do pól przez ograniczenie nie działa. Nawet jeśli każdy typ w unii ma pole ID, v.ID jest niedozwolone. Zamiast tego użyj metody w ograniczeniu.

Kiedy nie używać generyków

Generyki pasują do typów kontenerowych (stosy, zbiory, cache, drzewa) i do algorytmów na slice'ach i mapach, które robią to samo dla każdego typu elementu. Nie pasują wszędzie.

  • Jeden konkretny typ. Jeśli funkcja jest zawsze wywoływana tylko z []User, wersja generyczna dodaje koszt czytania i nic poza tym.
  • Zachowanie zależne od typu. Do tego służą interfejsy. Funkcja przyjmująca io.Writer jest prostsza niż taka, która przyjmuje [T io.Writer], i działa tak samo.
  • Zastępowanie parametrów interfejsowych. func Print[T fmt.Stringer](v T) nie jest lepsze od func Print(v fmt.Stringer).

Wskazówka samego zespołu Go: najpierw napisz kod dla konkretnego typu, a po parametry typów sięgnij, gdy zauważysz, że drugi raz piszesz ten sam kod, w którym zmieniają się tylko typy.

Typowe błędy

  • Używanie any tam, gdzie potrzebne jest comparable lub cmp.Ordered. Kompilator odrzuca == lub < na parametrze typu any.
  • Zapomniana ~. Typy zdefiniowane przez użytkownika, jak type ID int, nie spełniają zwykłego ograniczenia int.
  • Zwracanie nil dla parametru typu. Niedozwolone, chyba że ograniczenie zawęża T do typów podobnych do wskaźników. Zwracaj wartość zerową.
  • Pisanie generycznej funkcji pomocniczej, która już istnieje. Najpierw sprawdź slices, maps i cmp.

Najczęściej zadawane pytania

Czy Go ma generyki?

Tak, od Go 1.18 (marzec 2022). Funkcje i typy mogą deklarować parametry typów w nawiasach kwadratowych: func Map[T, U any](s []T, f func(T) U) []U. Każdy parametr typu ma ograniczenie, czyli interfejs opisujący, co typ musi obsługiwać.

Czym różni się any od comparable w generykach Go?

any dopuszcza każdy typ, ale z wartościami pozwala zrobić niewiele poza przypisywaniem, przekazywaniem i przechowywaniem. comparable dopuszcza tylko typy obsługujące == i !=, czyli to, czego potrzebujesz do kluczy mapy lub sprawdzania równości. Do < i > użyj cmp.Ordered.

Co oznacza tylda (~) w ograniczeniu w Go?

~T oznacza "dowolny typ, którego typem bazowym jest T". ~int pasuje do int, a także do type Celsius int. Bez tyldy int w ograniczeniu pasuje tylko do samego int, więc typy zdefiniowane przez użytkownika byłyby odrzucane.

Czy metody w Go mogą mieć parametry typów?

Nie. Metoda może używać parametrów typów swojego typu odbiorcy (func (s *Stack[T]) Push(v T)), ale nie może deklarować nowych. Zamiast tego napisz generyczną funkcję na najwyższym poziomie: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ