Heim >Backend-Entwicklung >Golang >Go Generics: Ein tiefer Einblick

Go Generics: Ein tiefer Einblick

Mary-Kate Olsen
Mary-Kate OlsenOriginal
2025-01-01 01:51:09979Durchsuche

Go Generics: A Deep Dive

1. Verzichten Sie auf Generika

Vor der Einführung von Generika gab es mehrere Ansätze zur Implementierung generischer Funktionen, die verschiedene Datentypen unterstützen:

Ansatz 1: Implementieren Sie eine Funktion für jeden Datentyp
Dieser Ansatz führt zu extrem redundantem Code und hohen Wartungskosten. Für jede Änderung muss für alle Funktionen derselbe Vorgang ausgeführt werden. Da die Go-Sprache außerdem das Überladen von Funktionen mit demselben Namen nicht unterstützt, ist es auch unpraktisch, diese Funktionen für externe Modulaufrufe verfügbar zu machen.

Ansatz 2: Verwenden Sie den Datentyp mit der größten Reichweite
Um Coderedundanz zu vermeiden, besteht eine andere Methode darin, den Datentyp mit dem größten Bereich zu verwenden, d. h. Ansatz 2. Ein typisches Beispiel ist math.Max, das die größere von zwei Zahlen zurückgibt. Um Daten verschiedener Datentypen vergleichen zu können, verwendet math.Max ​​float64, den Datentyp mit dem größten Bereich unter den numerischen Typen in Go, als Eingabe- und Ausgabeparameter und vermeidet so Präzisionsverluste. Obwohl dadurch das Code-Redundanzproblem bis zu einem gewissen Grad gelöst wird, muss jeder Datentyp zunächst in den Typ float64 konvertiert werden. Wenn Sie beispielsweise int mit int vergleichen, ist immer noch eine Typumwandlung erforderlich, was nicht nur die Leistung beeinträchtigt, sondern auch unnatürlich erscheint.

Ansatz 3: Verwenden Sie den Typ interface{}
Die Verwendung des Typs interface{} löst die oben genannten Probleme effektiv. Der Typ interface{} führt jedoch zu einem gewissen Laufzeitaufwand, da er zur Laufzeit Typzusicherungen oder Typbeurteilungen erfordert, was zu einer gewissen Leistungseinbuße führen kann. Darüber hinaus kann der Compiler bei Verwendung des Typs interface{} keine statische Typprüfung durchführen, sodass einige Typfehler möglicherweise erst zur Laufzeit entdeckt werden.

2. Vorteile von Generika

Go 1.18 führte die Unterstützung für Generika ein, was eine bedeutende Änderung seit der Open-Source-Bereitstellung der Go-Sprache darstellt.
Generics sind ein Merkmal von Programmiersprachen. Es ermöglicht Programmierern, bei der Programmierung generische Typen anstelle tatsächlicher Typen zu verwenden. Anschließend werden die generischen Typen durch explizite Übergabe oder automatische Ableitung während tatsächlicher Aufrufe ersetzt, wodurch der Zweck der Code-Wiederverwendung erreicht wird. Bei der Verwendung von Generika wird der zu bearbeitende Datentyp als Parameter angegeben. Solche Parametertypen werden als generische Klassen, generische Schnittstellen und generische Methoden in Klassen, Schnittstellen bzw. Methoden bezeichnet.
Die Hauptvorteile von Generika sind die Verbesserung der Wiederverwendbarkeit des Codes und der Typsicherheit. Im Vergleich zu herkömmlichen formalen Parametern machen Generika das Schreiben von universellem Code prägnanter und flexibler, bieten die Möglichkeit, verschiedene Datentypen zu verarbeiten und verbessern die Ausdruckskraft und Wiederverwendbarkeit der Go-Sprache weiter. Da die spezifischen Typen von Generika zur Kompilierzeit bestimmt werden, kann gleichzeitig eine Typprüfung durchgeführt werden, wodurch Typkonvertierungsfehler vermieden werden.

3. Unterschiede zwischen Generics und Schnittstelle{}

In der Go-Sprache sind sowohl Interface{} als auch Generics Werkzeuge für den Umgang mit mehreren Datentypen. Um ihre Unterschiede zu diskutieren, werfen wir zunächst einen Blick auf die Implementierungsprinzipien von Interface{} und Generics.

3.1 Schnittstelle{} Implementierungsprinzip

interface{} ist eine leere Schnittstelle ohne Methoden im Schnittstellentyp. Da alle Typen interface{} implementieren, können damit Funktionen, Methoden oder Datenstrukturen erstellt werden, die jeden Typ akzeptieren können. Die zugrunde liegende Struktur von interface{} zur Laufzeit wird als eface dargestellt, dessen Struktur unten dargestellt ist und hauptsächlich zwei Felder enthält, _type und data.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

_type ist ein Zeiger auf die _type-Struktur, die Informationen wie Größe, Art, Hash-Funktion und Zeichenfolgendarstellung des tatsächlichen Werts enthält. data ist ein Zeiger auf die tatsächlichen Daten. Wenn die Größe der tatsächlichen Daten kleiner oder gleich der Größe eines Zeigers ist, werden die Daten direkt im Datenfeld gespeichert. andernfalls speichert das Datenfeld einen Zeiger auf die tatsächlichen Daten.
Wenn ein Objekt eines bestimmten Typs einer Variablen vom Typ interface{} zugewiesen wird, führt die Go-Sprache implizit die Boxing-Operation von eface aus und setzt das Feld _type auf den Typ des Werts und das Datenfeld auf die Daten des Werts . Wenn beispielsweise die Anweisung var i interface{} = 123 ausgeführt wird, erstellt Go eine Eface-Struktur, wobei das Feld _type den Typ int und das Feld data den Wert 123 darstellt.
Beim Abrufen des gespeicherten Werts von der Schnittstelle {} findet ein Unboxing-Prozess statt, dh eine Typzusicherung oder Typbeurteilung. Dieser Prozess erfordert die explizite Angabe des erwarteten Typs. Wenn der Typ des in interface{} gespeicherten Werts mit dem erwarteten Typ übereinstimmt, ist die Typzusicherung erfolgreich und der Wert kann abgerufen werden. Andernfalls schlägt die Typzusicherung fehl und in dieser Situation ist eine zusätzliche Behandlung erforderlich.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

Es ist ersichtlich, dass die Schnittstelle{} Operationen an mehreren Datentypen durch Boxing- und Unboxing-Operationen zur Laufzeit unterstützt.

3.2 Prinzip der Generika-Implementierung

Das Go-Kernteam war bei der Bewertung der Implementierungsschemata von Go-Generika sehr vorsichtig. Insgesamt wurden drei Umsetzungspläne eingereicht:

  • Schablonenschema
  • Wörterbücher-Schema
  • GC-Form-Schablonenschema

Das Schablonenschema ist auch das Implementierungsschema, das von Sprachen wie C und Rust zur Implementierung von Generika übernommen wird. Sein Implementierungsprinzip besteht darin, dass während des Kompilierungszeitraums entsprechend den spezifischen Typparametern beim Aufruf der generischen Funktion oder den Typelementen in den Einschränkungen für jedes Typargument eine separate Implementierung der generischen Funktion generiert wird, um Typsicherheit und optimale Leistung zu gewährleisten . Diese Methode verlangsamt jedoch den Compiler. Denn wenn viele Datentypen aufgerufen werden, muss die generische Funktion für jeden Datentyp unabhängige Funktionen generieren, was zu sehr großen kompilierten Dateien führen kann. Gleichzeitig kann der generierte Code aufgrund von Problemen wie CPU-Cache-Fehlern und der Vorhersage von Befehlsverzweigungen möglicherweise nicht effizient ausgeführt werden.

Das Dictionaries-Schema generiert nur eine Funktionslogik für die generische Funktion, fügt der Funktion jedoch ein Parameter-Dikt als ersten Parameter hinzu. Der dict-Parameter speichert die typbezogenen Informationen der Typargumente beim Aufruf der generischen Funktion und übergibt die Wörterbuchinformationen mithilfe des AX-Registers (AMD) während des Funktionsaufrufs. Der Vorteil dieses Schemas besteht darin, dass es den Overhead der Kompilierungsphase reduziert und die Größe der Binärdatei nicht erhöht. Es erhöht jedoch den Laufzeitaufwand, kann in der Kompilierungsphase keine Funktionsoptimierung durchführen und weist Probleme wie die Wörterbuchrekursion auf.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

Go hat schließlich die beiden oben genannten Schemata integriert und das GC Shape Schablonenschema für die generische Implementierung vorgeschlagen. Es generiert Funktionscode in Einheiten der GC-Form eines Typs. Typen mit derselben GC-Form verwenden denselben Code wieder (die GC-Form eines Typs bezieht sich auf seine Darstellung im Go-Speicherzuweiser/Garbage Collector). Alle Zeigertypen verwenden den Typ *uint8 wieder. Für Typen mit derselben GC-Form wird ein gemeinsam genutzter instanziierter Funktionscode verwendet. Dieses Schema fügt außerdem jedem instanziierten Funktionscode automatisch einen dict-Parameter hinzu, um verschiedene Typen mit derselben GC-Form zu unterscheiden.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

3.3 Unterschiede

Anhand der zugrunde liegenden Implementierungsprinzipien von Interface{} und Generics können wir feststellen, dass der Hauptunterschied zwischen ihnen darin besteht, dass Interface{} die Verarbeitung verschiedener Datentypen während der Laufzeit unterstützt, während Generics die statische Verarbeitung verschiedener Datentypen in der Kompilierungsphase unterstützen. Im praktischen Einsatz gibt es vor allem folgende Unterschiede:
(1) Leistungsunterschied: Die Boxing- und Unboxing-Vorgänge, die ausgeführt werden, wenn unterschiedliche Datentypen der Schnittstelle{} zugewiesen oder von ihr abgerufen werden, sind kostspielig und führen zu zusätzlichem Overhead. Im Gegensatz dazu erfordern Generics keine Boxing- und Unboxing-Vorgänge, und der von Generics generierte Code ist für bestimmte Typen optimiert, wodurch ein Overhead bei der Laufzeitleistung vermieden wird.
(2) Typsicherheit: Bei Verwendung des Typs interface{} kann der Compiler keine statische Typprüfung durchführen und nur Typzusicherungen zur Laufzeit durchführen. Daher werden einige Typfehler möglicherweise erst zur Laufzeit entdeckt. Im Gegensatz dazu wird der generische Code von Go zur Kompilierungszeit generiert, sodass der generische Code zur Kompilierungszeit Typinformationen erhalten kann, wodurch die Typsicherheit gewährleistet wird.

4. Szenarien für Generika

4.1 Anwendbare Szenarien

  • Bei der Implementierung allgemeiner Datenstrukturen: Durch die Verwendung von Generika können Sie Code einmal schreiben und ihn für verschiedene Datentypen wiederverwenden. Dies reduziert die Codeduplizierung und verbessert die Wartbarkeit und Erweiterbarkeit des Codes.
  • Bei der Arbeit mit nativen Containertypen in Go: Wenn eine Funktion Parameter von in Go integrierten Containertypen wie Slices, Maps oder Kanälen verwendet und der Funktionscode keine spezifischen Annahmen über die Elementtypen in den Containern trifft Durch die Verwendung von Generika können die Containeralgorithmen vollständig von den Elementtypen in den Containern entkoppelt werden. Bevor die generische Syntax verfügbar war, wurde normalerweise Reflektion für die Implementierung verwendet, aber Reflektion macht den Code weniger lesbar, kann keine statische Typprüfung durchführen und erhöht den Laufzeitaufwand des Programms erheblich.
  • Wenn die Logik der für verschiedene Datentypen implementierten Methoden dieselbe ist: Wenn Methoden verschiedener Datentypen dieselbe Funktionslogik haben und der einzige Unterschied im Datentyp der Eingabeparameter besteht, können Generika verwendet werden, um die Coderedundanz zu reduzieren.

4.2 Nicht anwendbare Szenarien

  • Ersetzen Sie Schnittstellentypen nicht durch Typparameter: Schnittstellen unterstützen einen bestimmten Sinn für generische Programmierung. Wenn die Operationen an Variablen bestimmter Typen nur die Methoden dieses Typs aufrufen, verwenden Sie einfach den Schnittstellentyp direkt, ohne Generika zu verwenden. Beispielsweise verwendet io.Reader eine Schnittstelle zum Lesen verschiedener Datentypen aus Dateien und Zufallszahlengeneratoren. io.Reader ist aus Code-Perspektive leicht zu lesen, hocheffizient und es gibt fast keinen Unterschied in der Effizienz der Funktionsausführung, sodass keine Typparameter verwendet werden müssen.
  • Wenn die Implementierungsdetails von Methoden für verschiedene Datentypen unterschiedlich sind: Wenn die Methodenimplementierung für jeden Typ unterschiedlich ist, sollte der Schnittstellentyp anstelle von Generika verwendet werden.
  • In Szenarien mit starker Laufzeitdynamik: Beispielsweise führt die direkte Verwendung von interface{} in Szenarios, in denen die Typbeurteilung mithilfe von switch durchgeführt wird, zu besseren Ergebnissen.

5. Fallen in Generika

5.1 Null-Vergleich

In der Go-Sprache dürfen Typparameter nicht direkt mit Null verglichen werden, da Typparameter zur Kompilierungszeit einer Typprüfung unterzogen werden, während Null zur Laufzeit ein spezieller Wert ist. Da der zugrunde liegende Typ des Typparameters zur Kompilierungszeit unbekannt ist, kann der Compiler nicht bestimmen, ob der zugrunde liegende Typ des Typparameters den Vergleich mit Null unterstützt. Um die Typsicherheit aufrechtzuerhalten und potenzielle Laufzeitfehler zu vermeiden, erlaubt die Go-Sprache daher keinen direkten Vergleich zwischen Typparametern und Null.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

5.2 Ungültige Basiselemente

Der Typ T des zugrunde liegenden Elements muss ein Basistyp sein und darf kein Schnittstellentyp sein.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

5.3 Ungültige Union-Typ-Elemente

Elemente vom Unionstyp können keine Typparameter sein und Nicht-Schnittstellenelemente müssen paarweise disjunkt sein. Wenn mehr als ein Element vorhanden ist, kann es weder einen Schnittstellentyp mit nicht leeren Methoden enthalten, noch kann es vergleichbar sein oder Vergleichbares einbetten.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

5.4 Schnittstellentypen können nicht rekursiv eingebettet werden

type Op interface{
       int|float 
}
func Add[T Op](m, n T) T { 
       return m + n
} 
// After generation =>
const dict = map[type] typeInfo{
       int : intInfo{
             newFunc,
             lessFucn,
             //......
        },
        float : floatInfo
} 
func Add(dict[T], m, n T) T{}

6. Best Practices

Um Generika sinnvoll nutzen zu können, sollten bei der Anwendung folgende Punkte beachtet werden:

  1. Vermeiden Sie übermäßige Verallgemeinerungen. Generika sind nicht für alle Szenarien geeignet und es muss sorgfältig abgewogen werden, in welchen Szenarien sie geeignet sind. Reflexion kann bei Bedarf verwendet werden: Go verfügt über Laufzeitreflexion. Der Reflexionsmechanismus unterstützt einen gewissen Sinn für generische Programmierung. Wenn bestimmte Vorgänge die folgenden Szenarien unterstützen müssen, kann eine Reflexion in Betracht gezogen werden: (1) Arbeiten mit Typen ohne Methoden, bei denen der Schnittstellentyp nicht anwendbar ist. (2) Wenn die Operationslogik für jeden Typ unterschiedlich ist, sind Generika nicht anwendbar. Ein Beispiel ist die Implementierung des Pakets „encoding/json“. Da nicht erwünscht ist, dass jeder zu kodierende Typ die MarshalJson-Methode implementiert, kann der Schnittstellentyp nicht verwendet werden. Und da die Kodierungslogik für verschiedene Typen unterschiedlich ist, sollten keine Generika verwendet werden.
  2. Verwenden Sie eindeutig *T, []T und map[T1]T2, anstatt T Zeigertypen, Slices oder Maps darstellen zu lassen. Anders als die Tatsache, dass Typparameter in C Platzhalter sind und durch echte Typen ersetzt werden, ist der Typ des Typparameters T in Go der Typparameter selbst. Daher führt die Darstellung als Zeiger, Slice, Karte und andere Datentypen während der Verwendung zu vielen unerwarteten Situationen, wie unten gezeigt:
type V interface{
        int|float|*int|*float
} 
func F[T V](m, n T) {}
// 1. Generate templates for regular types int/float
func F[go.shape.int_0](m, n int){} 
func F[go.shape.float_0](m, n int){}
// 2. Pointer types reuse the same template
func F[go.shape.*uint8_0](m, n int){}
// 3. Add dictionary passing during the call
const dict = map[type] typeInfo{
        int : intInfo{},
        float : floatInfo{}
} 
func F[go.shape.int_0](dict[int],m, n int){}

Der obige Code meldet einen Fehler: Ungültige Operation: Zeiger von ptr (Variable vom Typ T, eingeschränkt durch *int | *uint) müssen identische Basistypen haben. Der Grund für diesen Fehler liegt darin, dass T ein Typparameter ist und der Typparameter kein Zeiger ist und die Dereferenzierungsoperation nicht unterstützt. Dies kann gelöst werden, indem die Definition wie folgt geändert wird:

// Wrong example
func ZeroValue0[T any](v T) bool {
    return v == nil  
}
// Correct example 1
func Zero1[T any]() T {
    return *new(T) 
}
// Correct example 2
func Zero2[T any]() T {
    var t T
    return t 
}
// Correct example 3
func Zero3[T any]() (t T) {
    return 
}

Zusammenfassung

Insgesamt lassen sich die Vorteile von Generika in drei Aspekten zusammenfassen:

  1. Typen werden während des Kompilierungszeitraums bestimmt, um die Typsicherheit zu gewährleisten. Was hineingegeben wird, wird herausgenommen.
  2. Die Lesbarkeit wurde verbessert. Der tatsächliche Datentyp ist aus der Codierungsphase explizit bekannt.
  3. Generika führen den Verarbeitungscode für denselben Typ zusammen, wodurch die Code-Wiederverwendungsrate verbessert und die allgemeine Flexibilität des Programms erhöht wird. Für allgemeine Datentypen sind Generika jedoch nicht zwingend erforderlich. Es muss dennoch sorgfältig abgewogen werden, ob Generika entsprechend der tatsächlichen Anwendungssituation eingesetzt werden sollen.

Leapcell: Die fortschrittliche Plattform für Go-Webhosting, Async-Aufgaben und Redis

Go Generics: A Deep Dive

Lassen Sie mich abschließend Leapcell vorstellen, die am besten geeignete Plattform für die Bereitstellung von Go-Diensten.

1. Mehrsprachige Unterstützung

  • Entwickeln Sie mit JavaScript, Python, Go oder Rust.

2. Stellen Sie unbegrenzt viele Projekte kostenlos bereit

  • Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.

3. Unschlagbare Kosteneffizienz

  • Pay-as-you-go ohne Leerlaufgebühren.
  • Beispiel: 25 $ unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.

4. Optimierte Entwicklererfahrung

  • Intuitive Benutzeroberfläche für mühelose Einrichtung.
  • Vollautomatische CI/CD-Pipelines und GitOps-Integration.
  • Echtzeitmetriken und Protokollierung für umsetzbare Erkenntnisse.

5. Mühelose Skalierbarkeit und hohe Leistung

  • Automatische Skalierung zur problemlosen Bewältigung hoher Parallelität.
  • Kein Betriebsaufwand – konzentrieren Sie sich nur auf das Bauen.

Erfahren Sie mehr in der Dokumentation!

Leapcell Twitter: https://x.com/LeapcellHQ

Das obige ist der detaillierte Inhalt vonGo Generics: Ein tiefer Einblick. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn