programowanieProgramista Backend

Jak zrealizowane są generyki (generics) w Go? Jakie są ograniczenia, składnia, pułapki i odpowiednie przypadku użycia?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Generyki, czyli generics, pojawiły się w Go od wersji 1.18. Przez długi czas Go był uważany za język konserwatywny, w którym z uwagi na prostotę wykluczono generyki, ale wraz z rosnącą liczbą projektów i rozwojem ekosystemu wzrosła potrzeba pisania uniwersalnych funkcji i struktur. Jest to szczególnie istotne dla struktur kontenerowych, algorytmów przetwarzania kolekcji i kodu infrastrukturalnego.

Problem: do czasu pojawienia się generyków trzeba było duplikować kod lub używać pustych interfejsów (interface{}), co prowadziło do utraty bezpieczeństwa typów i zmniejszenia wydajności, a także komplikacji w debugowaniu.

Rozwiązanie: generyki są wdrażane w Go przy pomocy parametrów typów, które są określane w nawiasach kwadratowych w funkcjach i typach. Dzięki constraints można ograniczać dozwolone parametry typów. Umożliwia to pisanie generycznych funkcji bez utraty bezpieczeństwa typów.

Przykład kodu:

package main import "fmt" type Adder[T any] func(a, b T) T func Sum[T any](slice []T, add Adder[T]) T { var result T for _, v := range slice { result = add(result, v) } return result } func intAdder(a, b int) int { return a + b } func main() { nums := []int{1, 2, 3, 4} sum := Sum(nums, intAdder) fmt.Println(sum) }

Kluczowe cechy:

  • Bezpieczeństwo typów — eliminacja błędów typów w czasie kompilacji.
  • Ograniczenia (constraints) — możliwość ograniczania generyków tylko do typów, które realizują określone interfejsy lub mają możliwość porównania.
  • Kompatybilność — nowi programiści mogą używać generyków w miarę potrzeb, nie przearanżowując projektów od razu.

Pytania z pułapką.

Czy można używać operacji arytmetycznych (+, -, *, /) dla dowolnych parametrów typu T w generykach?

Nie. Kompilator Go nie wie, czy typ parametru obsługuje arytmetykę. W tym celu trzeba określić constraint, na przykład interfejs z operator constraints od Go 1.18+

Przykład kodu:

type Addable interface { int | float64 | uint } func Sum[T Addable](slice []T) T { var result T for _, v := range slice { result += v } return result }

Czy generyczne kontenery mogą zawierać metody o różnym zachowaniu dla różnych typów?

Nie. Metody generycznych struktur lub funkcji są identyczne dla wszystkich typów, chyba że używa się type switch wewnątrz metody. Zachowanie musi być definiowane przez constraints lub być całkowicie parametryzowane.

Czy można tworzyć typy z parametrami typu (typy generyczne) na poziomie pakietu, a nie tylko funkcji?

Tak, od Go 1.18 można tworzyć zarówno generyczne funkcje, jak i generyczne typy strukturalne:

type Stack[T any] struct { items []T } func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }

Błędy typowe i antywzorce

  • Użycie interface{} zamiast generyków w nowych wersjach Go.
  • Zapominanie o określeniu ograniczeń (constraints), co prowadzi do niemożności użycia operacji w uniwersalnym kodzie.
  • Nadmierne uogólnianie prostych funkcji (generyki dla samego uogólnienia).

Przykład z życia

Negatywny przypadek

Wewnętrznie w bibliotece do pracy z kolekcjami zrealizowano uniwersalny funkcjonalność Map przy użyciu interface{}:

Zalety:

  • Uniwersalne, można używać dla dowolnych typów.

Wady:

  • Brak bezpieczeństwa typów, konieczność ręcznego rzutowania typów, błędy w czasie wykonywania.

Pozytywny przypadek

W tym samym projekcie przeszli na generyki i określili ograniczenia przez interfejsy:

Zalety:

  • Bezpieczeństwo typów, błędy wykrywane w czasie kompilacji, uproszczenie wsparcia.

Wady:

  • Wymagana znajomość nowej składni, niektóre IDE słabo wspierają skomplikowane constraints.