Programmiersprache: Generics in Go

Viele Eigenschaften in Go sind ein willkommener Kontrast zu anderen Sprachen. Für das Fehlen generischer Programmierung gab es bislang aber keine gute Antwort.

In Pocket speichern vorlesen Druckansicht 87 Kommentare lesen
Lesezeit: 14 Min.
Von
  • Rainer Stropek
Inhaltsverzeichnis

Seit Jahren diskutiert die Go-Gemeinde darüber, wie Generics in Go funktionieren sollten. In den letzten Monaten wurden mehrere Designentwürfe veröffentlicht, der letzte Anfang Januar 2021. Dieser Artikel fasst die wichtigsten Ansätze des aktuellen Designentwurfs zusammen und erklärt die Konzepte anhand von Codebeispielen. Darüber hinaus erfahren Entwicklerinnen und Entwickler, die schon jetzt mit Generics in Go experimentieren möchten, welche Wege es derzeit dafür gibt.

Eine der charakteristischen Eigenschaften der Sprache Go ist Einfachheit. Die Möglichkeit generischer Programmierung erhöht die Komplexität, und daher hatten sich die Go-Designer entschieden, dieses Feature wegzulassen. Dadurch blieben die Sprache schlank, der Compiler einfach und der aus Go resultierende, ausführbare Code performant.

Die Entscheidung, Generics wegzulassen, hat Konsequenzen. Generische Datenstrukturen wie Listen, Graphen oder Baumstrukturen sind für jeden konkreten Datentyp, den sie verwalten sollten, immer wieder aufs Neue zu implementieren. Gleiches gilt für Algorithmen wie Sortieren, Set-Operationen und Map/Reduce.

Da man versucht, diese manuelle Arbeit in der Praxis zu vermeiden, haben sich zwei Lösungsansätze etabliert. Eine Möglichkeit ist, dass man den Go-Code für die konkreten Typen oder Algorithmen mit go generate und der darauf aufbauenden Tools wie genny generiert. Man bekommt auf diese Weise zwar typsichere Datenstrukturen und auf ihnen operierende Algorithmen, der dafür notwendige, zusätzliche Schritt des Generierens von Code ist aber unbequem. Die Sprache Go bleibt einfach, die Komplexität verschwindet aber nicht, sondern wird in den Generator verschoben.

Wer den Generator-Ansatz nicht mag, kann bis zu einem gewissen Grad auf Typsicherheit verzichten und stattdessen das leere Interface interface{} verwenden. Es verlangt – wie aus der Typdefinition ersichtlich – die Implementierung mindestens keiner Methode. Jeder Datentyp in Go ist daher kompatibel mit dem leeren Interface. Es kommt aus diesem Grund in Fällen zum Einsatz, in denen man mit zur Entwicklungszeit unbekannten Datentypen arbeiten muss. Ein Beispiel für diesen Ansatz ist das Go-Package container/list, das unter anderem eine doppelt verkettete Liste zur Verfügung stellt. Die Listenelemente sind vom Typ Element, und dieses verweist auf den jeweiligen, enthaltenen Wert mit dem leeren Interface. Der folgende Codeausschnitt zeigt die Definition von Element:

type Element struct {
    // The value stored with this element.
    Value interface{}
}

Das leere Interface macht zwar die Codegenerierung obsolet, hat aber Nachteile zur Laufzeit. Will man auf den konkreten Typ zugreifen, benötigt man Type Assertions. Der folgende Codeausschnitt zeigt an einem einfachen Beispiel, wie sie sich verwenden lassen, um zur Laufzeit zu prüfen, ob eine Variable vom Typ interface{} tatsächlich kompatibel mit einem spezifischeren Datentyp ist:

type hero struct {
    name   string
    canFly bool
}

// Add the required method for the fmt.Stringer interface.
func (h hero) String() string {
    return h.name
}

var something interface{} = hero{name: "Homelander", canFly: true}
h, ok := something.(hero) // Use type assertation to check if something is a hero
if ok {
    fmt.Println(h.name)
}

// Use type assertation to check if something fulfills the 
// requirements of the fmt.Stringer interface.
var hStringer fmt.Stringer = something.(fmt.Stringer)
fmt.Println(hStringer.String())