Überblick über Codegenerierung
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 84 von 107.
Codegenerierung ist eine Alternative zur Reflection, die Go-Quellcode zur Kompilierzeit erstellt, anstatt Typen zur Laufzeit zu untersuchen. Dieser Ansatz bietet dir Typsicherheit und eine bessere Leistung und reduziert gleichzeitig sich wiederholenden Boilerplate-Code.
Go stellt den Befehl go generate bereit, um die Codegenerierung zu automatisieren. Du platzierst eine spezielle Kommentardirektive in deiner Quelldatei, und beim Ausführen von go generate wird das angegebene Tool ausgeführt:
//go:generate stringer -type=Status
type Status int
const (
Pending Status = iota
Active
Completed
)Das Ausführen von go generate ./... ruft das Tool stringer auf, das eine neue Datei mit einer String()-Methode für den Typ Status erstellt. Der generierte Code ist gewöhnlicher Go-Code, der zusammen mit deinem Programm kompiliert wird.
Zu den gängigen Werkzeugen zur Codegenerierung im Go-Ökosystem gehören stringer für String-Methoden von Enumerationen, mockgen zum Erstellen von Test-Mocks aus Interfaces und protoc zum Generieren von Code aus Protocol-Buffer-Definitionen. Viele ORMs und API-Frameworks verwenden die Generierung ebenfalls, um typsichere Datenbankabfragen oder Client-Code zu erstellen.
Der entscheidende Vorteil gegenüber Reflection besteht darin, dass generierter Code vom Compiler geprüft wird. Fehler werden zur Build-Zeit und nicht zur Laufzeit erkannt. Der Nachteil ist ein zusätzlicher Build-Schritt sowie generierte Dateien, die du in deinem Repository verwalten musst. Für OOP-Muster, die sich wiederholende Implementierungen für viele Typen erfordern, bietet die Codegenerierung oft das beste Gleichgewicht zwischen Sicherheit und Komfort.
Aufgabe
EinfachErstellen wir einen Codegenerator, der String()-Methoden für enumartige Typen erzeugt! Obwohl wir in dieser Umgebung kein tatsächliches go generate ausführen können, simulierst du, was Codegenerierungstools tun: Go-Quellcode programmgesteuert auf Grundlage von Typdefinitionen erzeugen.
Du organisierst deinen Code über zwei Dateien:
generator.go: Erstelle deine Logik zur Codegenerierung.Erstelle eine
EnumType-Struktur, die eine enumartige Typdefinition mit einemName(String) undValues(Slice von Strings, die die Namen der Konstanten darstellen) repräsentiert.Erstelle eine Funktion
GenerateStringMethod(e EnumType) string, die Go-Quellcode für eineString()-Methode erzeugt. Der generierte Code sollte diesem Muster folgen:func (e TypeName) String() string { switch e { case Value1: return "Value1" case Value2: return "Value2" default: return "Unknown" } }Erstelle außerdem eine Funktion
GenerateConstants(e EnumType) string, die mithilfe voniotadie Konstantendeklarationen erzeugt:const ( Value1 TypeName = iota Value2 Value3 )Erstelle schließlich
GenerateFullCode(e EnumType) string, das beide Ausgaben mit einem Kopfkommentar kombiniert, der angibt, dass es sich um generierten Code handelt:// Code generated by enum generator. DO NOT EDIT. [constants] [string method]main.go: Verwende deinen Generator, um Code für verschiedene enumartige Typen zu erzeugen.Lies den Namen des enumartigen Typs ein, danach eine Anzahl von Werten und anschließend jeden Wertnamen. Übergib diese Informationen an deinen Generator und gib den vollständig generierten Code aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Name des enumartigen Typs
- Zeile 2: Anzahl der Werte
- Folgende Zeilen: Jeder Wertname
Zum Beispiel bei:
Status
3
Pending
Active
Completedsollte deine Ausgabe folgendermaßen aussehen:
// Code generated by enum generator. DO NOT EDIT.
const (
Pending Status = iota
Active
Completed
)
func (e Status) String() string {
switch e {
case Pending:
return "Pending"
case Active:
return "Active"
case Completed:
return "Completed"
default:
return "Unknown"
}
}Und bei:
Priority
2
Low
Highsollte deine Ausgabe folgendermaßen aussehen:
// Code generated by enum generator. DO NOT EDIT.
const (
Low Priority = iota
High
)
func (e Priority) String() string {
switch e {
case Low:
return "Low"
case High:
return "High"
default:
return "Unknown"
}
}Diese Übung veranschaulicht das grundlegende Konzept hinter Tools wie stringer: Wiederholenden, aber typsicheren Code zur Kompilierzeit zu erzeugen, anstatt zur Laufzeit Reflection zu verwenden. Dein Generator erzeugt gültigen Go-Code, der in einer Datei gespeichert und zusammen mit dem restlichen Projekt kompiliert werden könnte.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Enum-Typnamen lesen
typeName, _ := reader.ReadString('\n')
typeName = strings.TrimSpace(typeName)
// Anzahl der Werte lesen
countStr, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countStr))
// Jeden Wertnamen lesen
values := make([]string, count)
for i := 0; i < count; i++ {
value, _ := reader.ReadString('\n')
values[i] = strings.TrimSpace(value)
}
// TODO: Erstelle ein EnumType mit den gelesenen Daten
// TODO: Generiere und drucke den vollständigen Code mit GenerateFullCode
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker Pool12Fortgeschrittene OOP-Konzepte
Functional-Options-PatternBuilder-Pattern in GoMethod ChainingTyp-Aliase vs. DefinitionenGrundlagen der ReflectionÜberblick über CodegenerierungÜbe selbstständig: Online-Go-Compiler