Generics in Go: type parameter anhand von Beispielen
1. Übersicht
Ab Go 1.18 können Sie Funktionen und Datentypen mit type parameter schreiben. Dadurch müssen Sie nicht dieselbe Logik für int, float64, string wiederholen und nicht mehr von interface{} casten.
Dieser Artikel geht vom ursprünglichen Problem zu generischen Funktionen, constraints und der Tilde-Notation ~ über. Dann behandeln wir generische Typen wie Stack[T] und Cache[K, V], das Schreiben von Tests sowie häufige Fehler. Nach der Lektüre wissen Sie, wann Sie generics verwenden sollten und wann ein normales interface ausreicht.
Alle Beispiele verwenden Go 1.22 oder höher. Das Package cmp ist ab Go 1.21 verfügbar. Minimale go.mod:
module example.com/shop
go 1.22
Keine externen Bibliotheken erforderlich. Alles in diesem Artikel ist in der Standardbibliothek enthalten.
2. Das Problem vor Generics
Angenommen, Sie müssen die Gesamtmenge der Artikel (int) und den Gesamtbetrag (float64) berechnen. Vor Go 1.18 schrieben Sie normalerweise zwei identische Funktionen:
func SumInts(xs []int) int {
var total int
for _, x := range xs {
total += x
}
return total
}
func SumFloats(xs []float64) float64 {
var total float64
for _, x := range xs {
total += x
}
return total
}
Die Alternative bestand darin, []interface{} zu akzeptieren und type switch zu verwenden. Dieser Ansatz verliert type safety. Fehler erscheinen nur zur Laufzeit, und Sie müssen überall beim Funktionsaufruf casten.
Generics lösen genau dieses Problem: Sie schreiben die Logik einmal, und der Compiler prüft die Typen für Sie.
3. Die erste generische Funktion
3.1. Syntax von type parameter
Type parameter werden in eckigen Klammern direkt nach dem Funktionsnamen deklariert:
package main
import "fmt"
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
func main() {
quantities := []int{2, 1, 5}
prices := []float64{120.5, 99.9, 15}
fmt.Println(Sum(quantities))
fmt.Println(Sum(prices))
}
Erklärung der einzelnen Teile:
[T Number]deklariert den type parameterT.Tmuss das constraintNumbererfüllen.Numberist eine interface, die eine type set enthält:int,int64,float64und Typen, die von ihnen abgeleitet sind (die Bedeutung von~finden Sie in Abschnitt 4.3).var total Terstellt den zero value vonT, also0oder0.0.- Der Operator
+=ist gültig, da alle Typen im type set Addition unterstützen.
Ausgabe:
8
235.4
3.2. Type Inference
Haben Sie bemerkt, dass wir Sum(quantities) aufrufen und nicht Sum[int](quantities)? Der Compiler leitet T aus den Argumenten ab. Das ist type inference.
Sie müssen den Typ nur explizit angeben, wenn der Compiler nicht genug Informationen hat, um ihn herzuleiten, z.B. wenn der type parameter nur im Rückgabetyp vorkommt:
func Zero[T any]() T {
var z T
return z
}
func main() {
fmt.Println(Zero[string]() == "") // true
}
3.3. Mehrere type parameter: Map und Filter
Dies sind die zwei am häufigsten verwendeten Utility-Funktionen. Wir wenden sie auf eine Liste von Order an:
package main
import "fmt"
type Order struct {
ID string
Customer string
Total float64
}
func Map[T, U any](xs []T, f func(T) U) []U {
out := make([]U, 0, len(xs))
for _, x := range xs {
out = append(out, f(x))
}
return out
}
func Filter[T any](xs []T, keep func(T) bool) []T {
var out []T
for _, x := range xs {
if keep(x) {
out = append(out, x)
}
}
return out
}
func main() {
orders := []Order{
{"A01", "Lan", 250_000},
{"A02", "Minh", 1_200_000},
{"A03", "Lan", 890_000},
}
big := Filter(orders, func(o Order) bool { return o.Total >= 500_000 })
ids := Map(big, func(o Order) string { return o.ID })
fmt.Println(ids)
}
Map[T, U any]hat zwei type parameter, daher können Ein- und Ausgabetyp unterschiedlich sein.anyist ein alias fürinterface{}. Es erlaubt jeden Typ, aber Sie können auf dem Wert keine Operatoren aufrufen außer Zuweisen und Weitergeben.- Der Compiler leitet
T = OrderundU = stringaus der Funktionfunc(o Order) stringab.
Ausgabe:
[A02 A03]
4. Constraint: Einschränkung, was T kann
Das constraint bestimmt, was Sie mit einem Wert vom Typ T in der Funktion tun können.
4.1. comparable
comparable ist ein vordefiniertes constraint. Es erlaubt die Verwendung von == und != und ist auch die Bedingung für T, um ein key einer map zu sein:
func Unique[T comparable](xs []T) []T {
seen := make(map[T]struct{}, len(xs))
var out []T
for _, x := range xs {
if _, ok := seen[x]; !ok {
seen[x] = struct{}{}
out = append(out, x)
}
}
return out
}
// Unique([]string{"Lan", "Minh", "Lan"}) => [Lan Minh]
Das Struct Order oben ist auch comparable, da alle seine Felder vergleichbar sind. Ein Struct, das eine slice, map oder func enthält, erfüllt nicht comparable, und der Compiler meldet einen Fehler.
4.2. cmp.Ordered
Wenn Sie Größer- oder Kleinvergleiche brauchen, verwenden Sie cmp.Ordered aus der Standardbibliothek:
import "cmp"
func MaxBy[T any, K cmp.Ordered](xs []T, key func(T) K) (T, bool) {
var best T
if len(xs) == 0 {
return best, false
}
best = xs[0]
for _, x := range xs[1:] {
if key(x) > key(best) {
best = x
}
}
return best, true
}
// top, _ := MaxBy(orders, func(o Order) float64 { return o.Total })
// top.ID => "A02"
K cmp.Orderederlaubt die Verwendung von>auf dem Schlüsselwert.- Die Funktion gibt
(T, bool)zurück, um leere slices zu handhaben, ohne zu paniken.
Hinweis: Bei einfachen numerischen slices müssen Sie nicht selbst schreiben. Go 1.21 hat die built-in Funktionen
max/min, und das Packagesliceshat bereitsslices.Max,slices.Contains,slices.Sort. Prüfen Sieslicesundmaps, bevor Sie eigene generische Utilities schreiben.
4.3. Die Tilde-Notation ~
In der Praxis definieren Sie häufig einen eigenen Typ für Geld:
type VND int64
func main() {
payments := []VND{150_000, 350_000}
fmt.Println(Sum(payments)) // 500000
}
Diese Zeile wird nur kompiliert, weil ~int64 in Number vorhanden ist. Die Notation ~int64 bedeutet „jeder Typ mit underlying type int64“. Wenn Sie int64 ohne ~ schreiben, erfüllt VND das constraint nicht, und der Compiler meldet:
VND does not satisfy Number (possibly missing ~ for int64 in Number)
Einfache Regel: Beim Schreiben von Constraints für Grundtypen sollten Sie fast immer ~ verwenden.
4.4. Constraint mit Methoden
Ein constraint kann auch Methoden verlangen, wie eine normale interface:
type Identifiable interface {
comparable
Key() string
}
Jeder Typ, der Identifiable erfüllen soll, muss sowohl vergleichbar als auch eine Methode Key() string haben.
5. Generische Typen
5.1. Stack[T]
Type parameter sind nicht nur für Funktionen, sondern auch für Structs:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}
func (s *Stack[T]) Len() int { return len(s.items) }
Verwendungsbeispiel:
var history Stack[Order]
history.Push(Order{ID: "A01"})
history.Push(Order{ID: "A02"})
o, _ := history.Pop()
fmt.Println(o.ID, history.Len()) // A02 1
- Der receiver wird als
*Stack[T]geschrieben und muss[T]enthalten. - Bei der Deklaration einer Variable müssen Sie den Typ explizit angeben:
Stack[Order]. Es gibt hier keine Inferenz. - Der zero value von
Stack[Order]funktioniert sofort, daappendgut mit nil slices funktioniert.
5.2. Cache[K, V] sicher für mehrere goroutine
Ein praxisorientiertes Beispiel: ein In-Memory-Cache, der von Customer und Order verwendet wird.
package cache
import "sync"
type Cache[K comparable, V any] struct {
mu sync.RWMutex
data map[K]V
}
func New[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{data: make(map[K]V)}
}
func (c *Cache[K, V]) Put(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
type Customer struct {
ID int
Name string
}
customers := cache.New[int, Customer]()
customers.Put(1, Customer{ID: 1, Name: "Lan"})
orders := cache.New[string, Order]()
orders.Put("A01", Order{ID: "A01", Total: 250_000})
c, ok := customers.Get(1)
fmt.Println(c.Name, ok) // Lan true
Eine Implementation bedient zwei Datentypen. customers.Get gibt genau Customer zurück, keine type assertion nötig. Falls Sie versehentlich einen Order in customers einfügen, wird der Code nicht kompiliert.
6. Tests für generischen Code schreiben
Go verwendet das Standard-Package testing. Table-driven Tests funktionieren normalerweise mit generischen Funktionen. Jede Testtabelle entspricht einem konkreten Typ:
package main
import "testing"
func TestSum(t *testing.T) {
ints := []struct {
name string
in []int
want int
}{
{"empty", nil, 0},
{"positive", []int{2, 1, 5}, 8},
{"mixed", []int{10, -3}, 7},
}
for _, tc := range ints {
t.Run(tc.name, func(t *testing.T) {
if got := Sum(tc.in); got != tc.want {
t.Errorf("Sum(%v) = %d, want %d", tc.in, got, tc.want)
}
})
}
if got := Sum([]VND{150_000, 350_000}); got != 500_000 {
t.Errorf("Sum VND = %d, want 500000", got)
}
}
func TestStackPopEmpty(t *testing.T) {
var s Stack[string]
v, ok := s.Pop()
if ok || v != "" {
t.Fatalf("Pop() on empty = (%q, %v), want (\"\", false)", v, ok)
}
}
Tests ausführen:
go test ./... -v
=== RUN TestSum
=== RUN TestSum/empty
=== RUN TestSum/positive
=== RUN TestSum/mixed
--- PASS: TestSum (0.00s)
=== RUN TestStackPopEmpty
--- PASS: TestStackPopEmpty (0.00s)
PASS
Testen Sie mindestens einen abgeleiteten Typ wie VND. Dies ist der schnellste Weg, um fehlende ~ in Constraints zu erkennen.
7. Häufige Fehler und wann man Generics nicht verwenden sollte
Methoden können keine eigenen type parameter haben. Der folgende Code wird nicht kompiliert:
// Fehler: Methoden können keine type parameter haben
func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U] { ... }
Schreiben Sie es stattdessen als top-level Funktion: func MapStack[T, U any](s *Stack[T], f func(T) U) *Stack[U].
Verwenden Sie kein type switch direkt auf T. Wenn Sie je nach Typ verzweigen möchten, müssen Sie zuerst zu any konvertieren: switch v := any(x).(type) { ... }. Wenn Sie das häufig tun, ist generics möglicherweise nicht das richtige Werkzeug.
Ein Constraint mit type set kann nicht als Variablentyp verwendet werden. var n Number meldet einen Fehler. Interfaces mit | oder ~ können nur als Constraint verwendet werden.
comparable mit interface kann panic auslösen. Wenn K ein interface type ist und der dynamische Wert darin eine slice ist, können Vergleiche oder die Verwendung als map key zur Laufzeit paniken.
Hinweis: Generics sind nicht automatisch schneller als interfaces. Der Go-Compiler verwendet die Technik „GC shape stenciling” mit einem dictionary. Verschiedene Zeigertypen können denselben Code gemeinsam nutzen, und method calls über constraints haben immer noch indirekten overhead. Wenn Performance wichtig ist, messen Sie mit benchmarks, anstatt zu spekulieren.
Wann man Generics NICHT verwenden sollte:
- Wenn die Logik von Verhalten (Methoden) abhängt, nicht vom Typ. Zum Beispiel nimmt
PaymentServiceeinenNotifiermit einer MethodeSend. Interfaces sind normalerweise prägnanter und lesbarer. - Wenn nur ein konkreter Typ die Funktion verwendet. Machen Sie es nicht „generisch für den Fall, dass es später nötig ist”.
- Wenn
slices,maps,cmpbereits die Funktionen haben, die Sie brauchen.
Eine leicht zu merkende Regel: Wenn Sie denselben Code für mehrere Typen schreiben und dieser nur im Typnamen unterscheidet, ist es Zeit für generics.
8. Fazit
Dieser Artikel behandelt die Hauptteile von generics in Go:
- Deklarieren Sie type parameter
[T C]und lassen Sie den Compiler den Typ selbst ableiten. - Verwenden Sie die Constraints
any,comparable,cmp.Orderedund definieren Sie eigene Constraints mit~, um abgeleitete Typen wieVNDzu unterstützen. - Erstellen Sie generische Typen
Stack[T]undCache[K, V], die sicher für mehrere goroutine sind. - Schreiben Sie table-driven Tests für generische Funktionen.
- Erkennen Sie die Grenzen: Methoden können keine eigenen type parameter haben, Constraints sind keine Variablentypen, und generics ersetzen interfaces nicht.
Beginnen Sie dort, wo Code wirklich in Ihrem Projekt wiederholt wird. Schreiben Sie generics minimal, bevorzugen Sie die Standardbibliothek, und testen Sie immer mit mindestens zwei verschiedenen Typen.