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:
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) }
Wewnętrznie w bibliotece do pracy z kolekcjami zrealizowano uniwersalny funkcjonalność Map przy użyciu interface{}:
Zalety:
Wady:
W tym samym projekcie przeszli na generyki i określili ograniczenia przez interfejsy:
Zalety:
Wady: