Um es klar zu sagen, ich werde allgemeine Richtlinien geben, keine festen Regeln. Nutzen Sie Ihr eigenes Urteilsvermögen. Aber wenn Sie unsicher sind, empfehle ich, die hier besprochenen Richtlinien zu verwenden.Um es klar zu sagen, ich werde allgemeine Richtlinien geben, keine festen Regeln. Nutzen Sie Ihr eigenes Urteilsvermögen. Aber wenn Sie unsicher sind, empfehle ich, die hier besprochenen Richtlinien zu verwenden.

Go: Wann sollten Sie Generics verwenden? Wann nicht?

2025/10/04 23:00

Projektvorstellung

Dies ist die Blog-Post-Version meiner Vorträge bei Google Open Source Live:

https://youtu.be/nr8EpUO9jhw?si=jlWTapr6NM6isLgt&embedable=true

und GopherCon 2021:

https://youtu.be/Pae9EeCdy8?si=M-87Eisb2nU1qmJ&embedable=true

\ Die Go 1.18-Version fügt eine wichtige neue Sprachfunktion hinzu: Unterstützung für generische Programmierung. In diesem Artikel werde ich nicht beschreiben, was Generics sind oder wie man sie verwendet. Dieser Artikel handelt davon, wann man Generics in Go-Code verwenden sollte und wann nicht.

\ Um es klar zu sagen: Ich werde allgemeine Richtlinien geben, keine festen Regeln. Nutzen Sie Ihr eigenes Urteilsvermögen. Aber wenn Sie sich nicht sicher sind, empfehle ich, die hier besprochenen Richtlinien zu verwenden.

Code schreiben

Beginnen wir mit einer allgemeinen Richtlinie für die Programmierung in Go: Schreiben Sie Go-Programme, indem Sie Code schreiben, nicht indem Sie Typen definieren. Wenn es um Generics geht und Sie beginnen, Ihr Programm zu schreiben, indem Sie Typparameter-Einschränkungen definieren, sind Sie wahrscheinlich auf dem falschen Weg. Beginnen Sie mit dem Schreiben von Funktionen. Es ist einfach, später Typparameter hinzuzufügen, wenn klar ist, dass sie nützlich sein werden.

Wann sind Typparameter nützlich?

Das gesagt, schauen wir uns Fälle an, in denen Typparameter nützlich sein können.

Bei der Verwendung von sprachdefinierten Container-Typen

Ein Fall ist, wenn Sie Funktionen schreiben, die mit den speziellen Container-Typen arbeiten, die von der Sprache definiert werden: Slices, Maps und Channels. Wenn eine Funktion Parameter mit diesen Typen hat und der Funktionscode keine besonderen Annahmen über die Elementtypen macht, kann es nützlich sein, einen Typparameter zu verwenden.

\ Hier ist zum Beispiel eine Funktion, die eine Slice aller Schlüssel in einer Map beliebigen Typs zurückgibt:

// MapKeys returns a slice of all the keys in m. // The keys are not returned in any particular order. func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {     s := make([]Key, 0, len(m))     for k := range m {         s = append(s, k)     }     return s } 

\ Dieser Code macht keine Annahmen über den Map-Schlüsseltyp und verwendet den Map-Werttyp überhaupt nicht. Er funktioniert für jeden Map-Typ. Das macht ihn zu einem guten Kandidaten für die Verwendung von Typparametern.

\ Die Alternative zu Typparametern für diese Art von Funktion ist typischerweise die Verwendung von Reflection, aber das ist ein umständlicheres Programmiermodell, wird nicht statisch zur Buildzeit typengeprüft und ist oft langsamer zur Laufzeit.

Allgemeine Datenstrukturen

Ein weiterer Fall, in dem Typparameter nützlich sein können, sind allgemeine Datenstrukturen. Eine allgemeine Datenstruktur ist etwas wie eine Slice oder Map, aber eine, die nicht in die Sprache eingebaut ist, wie eine verkettete Liste oder ein Binärbaum.

\ Heutzutage tun Programme, die solche Datenstrukturen benötigen, typischerweise eines von zwei Dingen: Sie schreiben sie mit einem spezifischen Elementtyp oder verwenden einen Schnittstellentyp. Das Ersetzen eines spezifischen Elementtyps durch einen Typparameter kann eine allgemeinere Datenstruktur erzeugen, die in anderen Teilen des Programms oder von anderen Programmen verwendet werden kann. Das Ersetzen eines Schnittstellentyps durch einen Typparameter kann es ermöglichen, Daten effizienter zu speichern und Speicherressourcen zu sparen; es kann auch ermöglichen, dass der Code Typbehauptungen vermeidet und zur Buildzeit vollständig typengeprüft wird.

\ Hier ist zum Beispiel ein Teil dessen, wie eine Binärbaum-Datenstruktur mit Typparametern aussehen könnte:

// Tree is a binary tree. type Tree[T any] struct {     cmp  func(T, T) int     root *node[T] }  // A node in a Tree. type node[T any] struct {     left, right  *node[T]     val          T }  // find returns a pointer to the node containing val, // or, if val is not present, a pointer to where it // would be placed if added. func (bt *Tree[T]) find(val T) **node[T] {     pl := &bt.root     for *pl != nil {         switch cmp := bt.cmp(val, (*pl).val); {         case cmp < 0:             pl = &(*pl).left         case cmp > 0:             pl = &(*pl).right         default:             return pl         }     }     return pl }  // Insert inserts val into bt if not already there, // and reports whether it was inserted. func (bt *Tree[T]) Insert(val T) bool {     pl := bt.find(val)     if *pl != nil {         return false     }     *pl = &node[T]{val: val}     return true } 

\ Jeder Knoten im Baum enthält einen Wert des Typparameters T. Wenn der Baum mit einem bestimmten Typargument instanziiert wird, werden Werte dieses Typs direkt in den Knoten gespeichert. Sie werden nicht als Schnittstellentypen gespeichert.

\ Dies ist eine vernünftige Verwendung von Typparametern, da die Tree-Datenstruktur, einschließlich des Codes in den Methoden, weitgehend unabhängig vom Elementtyp T ist.

\ Die Tree-Datenstruktur muss wissen, wie Werte des Elementtyps T verglichen werden; sie verwendet dafür eine übergebene Vergleichsfunktion. Sie können dies in der vierten Zeile der find-Methode sehen, im Aufruf von bt.cmp. Abgesehen davon spielt der Typparameter überhaupt keine Rolle.

Für Typparameter Funktionen gegenüber Methoden bevorzugen

Das Tree-Beispiel veranschaulicht eine weitere allgemeine Richtlinie: Wenn Sie etwas wie eine Vergleichsfunktion benötigen, bevorzugen Sie eine Funktion gegenüber einer Methode.

\ Wir hätten den Tree-Typ so definieren können, dass der Elementtyp eine Compare- oder Less-Methode haben muss. Dies würde durch das Schreiben einer Einschränkung erfolgen, die die Methode erfordert, was bedeutet, dass jedes Typargument, das zur Instanziierung des Tree-Typs verwendet wird, diese Methode haben müsste.

\ Eine Konsequenz wäre, dass jeder, der Tree mit einem einfachen Datentyp wie int verwenden möchte, seinen eigenen Integer-Typ definieren und seine eigene Vergleichsmethode schreiben müsste. Wenn wir Tree so definieren, dass es eine Vergleichsfunktion akzeptiert, wie im obigen Code gezeigt, dann ist es einfach, die gewünschte Funktion zu übergeben. Es ist genauso einfach, diese Vergleichsfunktion zu schreiben, wie eine Methode zu schreiben.

\ Wenn der Tree-Elementtyp zufällig bereits eine Compare-Methode hat, können wir einfach einen Methodenausdruck wie ElementType.Compare als Vergleichsfunktion verwenden.

\ Anders ausgedrückt, es ist viel einfacher, eine Methode in eine Funktion umzuwandeln, als eine Methode zu einem Typ hinzuzufügen. Bevorzugen Sie also für allgemeine Datentypen eine Funktion, anstatt eine Einschränkung zu schreiben, die eine Methode erfordert.

Implementierung einer gemeinsamen Methode

Ein weiterer Fall, in dem Typparameter nützlich sein können, ist, wenn verschiedene Typen eine gemeinsame Methode implementieren müssen und die Implementierungen für die verschiedenen Typen alle gleich aussehen.

\ Betrachten Sie zum Beispiel die sort.Interface der Standardbibliothek. Sie erfordert, dass ein Typ drei Methoden implementiert: Len, Swap und Less.

\ Hier ist ein Beispiel für einen generischen Typ SliceFn, der sort.Interface für jeden Slice-Typ implementiert:

// SliceFn implements sort.Interface for a slice of T. type SliceFn[T any] struct {     s    []T     less func(T, T) bool }  func (s SliceFn[T]) Len() int {     return len(s.s) } func (s SliceFn[T]) Swap(i, j int) {     s.s[i], s.s[j] = s.s[j], s.s[i] } func (s SliceFn[T]) Less(i, j int) bool {     return s.less(s.s[i], s.s[j]) } 

\ Für jeden Slice-Typ sind die Methoden Len und Swap genau gleich. Die Less-Methode erfordert einen Vergleich, was der Fn-Teil des Namens SliceFn ist. Wie beim früheren Tree-Beispiel werden wir eine Funktion übergeben, wenn wir ein SliceFn erstellen.

\ Hier ist, wie man SliceFn verwendet, um einen beliebigen Slice mit einer Vergleichsfunktion zu sortieren:

// SortFn sorts s in place using a comparison function. func SortFn[T any](s []T, less func(T, T) bool) {     sort.Sort(SliceFn[T]{s, less}) } 

\ Dies ähnelt der Standardbibliotheksfunktion sort.Slice, aber die Vergleichsfunktion wird mit Werten statt mit Slice-Indizes geschrieben.

\ Die Verwendung von Typparametern für diese Art von Code ist angemessen, weil die Methoden für alle Slice-Typen genau gleich aussehen.

\ (Ich sollte erwähnen, dass Go 1.19 – nicht 1.18 – höchstwahrscheinlich eine generische Funktion zum Sortieren eines Slice mit einer Vergleichsfunktion enthalten wird, und diese generische Funktion wird höchstwahrscheinlich nicht sort.Interface verwenden. Siehe Vorschlag #47619. Aber der allgemeine Punkt ist immer noch wahr, auch wenn dieses spezifische Beispiel höchstwahrscheinlich nicht nützlich sein wird: Es ist vernünftig, Typparameter zu verwenden, wenn Sie Methoden implementieren müssen, die für alle relevanten Typen gleich aussehen.)

Wann sind Typparameter nicht nützlich?

Sprechen wir nun über die andere Seite der Frage: Wann sollte man keine Typparameter verwenden?

Ersetzen Sie keine Schnittstellentypen durch Typparameter

Wie wir alle wissen, hat Go Schnittstellentypen. Schnittstellentypen ermöglichen eine Art generische Programmierung.

\ Zum Beispiel bietet die weit verbreitete io.Reader-Schnittstelle einen generischen Mechanismus zum Lesen von Daten aus jedem Wert, der Informationen enthält (zum Beispiel eine Datei) oder der Informationen produziert (zum Beispiel ein Zufallszahlengenerator). Wenn Sie mit einem Wert eines bestimmten Typs nur eine Methode aufrufen müssen, verwenden Sie einen Schnittstellentyp, keinen Typparameter. io.Reader ist leicht zu lesen, effizient und effektiv. Es besteht keine Notwendigkeit, einen Typparameter zu verwenden, um Daten aus einem Wert zu lesen, indem die Read-Methode aufgerufen wird.

\ Es könnte zum Beispiel verlockend sein, die erste Funktionssignatur hier, die nur einen Schnittstellentyp verwendet, in die zweite Version zu ändern, die einen Typparameter verwendet.

func ReadSome(r io.Reader) ([]byte, error)  func ReadSome[T io.Reader](r T) ([]byte, error) 

\ Machen Sie diese Art von Änderung nicht. Das Weglassen des Typparameters macht die Funktion einfacher zu schreiben, einfacher zu lesen, und die Ausführungszeit wird wahrscheinlich die gleiche sein.

\ Es lohnt sich, den letzten Punkt zu betonen. Während es möglich ist, Generics auf verschiedene Arten zu implementieren, und Implementierungen sich im Laufe der Zeit ändern und verbessern werden, wird die in Go 1.18 verwendete Implementierung in vielen Fäl

Haftungsausschluss: Die auf dieser Website veröffentlichten Artikel stammen von öffentlichen Plattformen und dienen ausschließlich zu Informationszwecken. Sie spiegeln nicht unbedingt die Ansichten von MEXC wider. Alle Rechte verbleiben bei den ursprünglichen Autoren. Sollten Sie der Meinung sein, dass Inhalte die Rechte Dritter verletzen, wenden Sie sich bitte an [email protected] um die Inhalte entfernen zu lassen. MEXC übernimmt keine Garantie für die Richtigkeit, Vollständigkeit oder Aktualität der Inhalte und ist nicht verantwortlich für Maßnahmen, die aufgrund der bereitgestellten Informationen ergriffen werden. Die Inhalte stellen keine finanzielle, rechtliche oder sonstige professionelle Beratung dar und sind auch nicht als Empfehlung oder Billigung von MEXC zu verstehen.