Go-Stil: lesbar und schnell
Das Ziel ist Code, den ein Betreuer von oben nach unten lesen kann, mit Leistung, die sich aus der Form der API und der Daten ergibt, nicht aus Tricks, die über den Code verteilt sind. Lesbarkeit und Geschwindigkeit stehen selten im Konflikt, sobald das Design stimmt; wenn doch, gib die Lesbarkeit nur auf einem gemessenen heißen Pfad auf und sag es in einem Kommentar.
Dies folgt Dave Cheneys "API-Design als Performance-Primitive": Über einen bestimmten Punkt hinaus kann Leistung nicht nachträglich hinzugefügt werden, also entwirf von Anfang an dafür, und halte alles andere schlicht.
Quellen: Cheneys Vortrag (https://dave.cheney.net/paste/gophercon-uk-2026.html), mit Code in github.com/pkg/json und github.com/davecheney/bytereader; John Ousterhout, "Define errors out of existence".
Prioritätenreihenfolge
- Korrektheit. Opfere sie niemals für Geschwindigkeit oder Kürze.
- API-Form und O-Notation. Wer besitzt den Speicher, was wird zurückgegeben, wie viel Arbeit verursacht jeder Aufruf. Diese entscheiden über die Leistung mehr als jede spätere Optimierung.
- Allokationen auf dem heißen Pfad. Jede Allokation kostet zweimal: einmal zum Allokieren, einmal für die Garbage Collection. Der Garbage Collector mag schnell sein, "aber nicht zu allokieren wird immer schneller sein."
- Arbeit pro Eingabeeinheit. Berühre jedes Byte einmal und mache Aufrufe pro Token oder Datensatz, nicht pro Byte.
- Mikrooptimierungen (manuelles Inlining, Bit-Tricks, Vermeidung von Bereichsprüfungen), nur auf einem Pfad, den ein Profiling als heiß zeigt, und nur mit einem Benchmark, der den Gewinn belegt.
Wenn eine Änderung auf der darüberliegenden Ebene nicht gerechtfertigt ist, führe sie nicht durch.
Heiße und kalte Pfade trennen
Entscheide, welcher Code pro Ereignis oder pro Anfrage (heiß) ausgeführt wird und welcher zur Einrichtung, Build-, Konfigurations- oder Aktualisierungszeit (kalt) läuft.
- Kalte Pfade: Schreibe den schlichtesten Code. Klonen statt Teilen, verwende Maps und
fmt, alloziere frei. Übernimm hier keine heißen Idiome; sie kosten Lesbarkeit und bringen nichts. - Heiße Pfade: Dürfen wiederverwendbare Puffer, Generationszähler, flache Arrays und ähnliche Techniken verwenden. Gib jeder Technik einen Namen (einen kleinen Typen oder Helfer) und einen Kommentar, der erklärt, warum sie existiert, damit ein Leser sie nicht reverse-engineeren muss.
Entwerfe die API für Leistung
Die API entscheidet, wie viel ein Aufrufer jemals einsparen kann. "Du benchmarkst den Garbage Collector", wenn die API jedes Ergebnis auf den Heap zwingt. Forme sie so, dass der schnelle Pfad der natürliche ist; ein Gewinn von 2-10x ist oft verfügbar, "wenn du bereit bist, eine andere API zu akzeptieren".
- Gib keine Werte zurück, die entkommen müssen. Die Rückgabe eines primitiven Typs durch
ein
interface{}/any, einen neuen String, eine Map oder einen Zeiger auf eine neue Struktur pro Element erzwingt eine Allokation pro Aufruf (encoding/json.Decoder.Tokenist der klassische "Müllfabrik"). Gib einen konkreten Typ oder eine Ansicht wie[]bytezurück und lasse den Aufrufer nur das konvertieren, was er braucht. - Gib Ansichten zurück, keine Kopien, wenn der Aufrufer nur lesen muss. Dokumentiere, wie
lange die Ansicht gültig ist ("gültig bis zum nächsten Aufruf", wie
bufio.Scanner.Bytes). Eine Ansicht der rohen Eingabe (die Bytes des Tokens, deren erstes Byte seine Art angibt) ist billiger als ein dekodierter Wert, den der Aufrufer vielleicht ignoriert. - Lasse den Aufrufer den wiederverwendbaren Speicher besitzen. Akzeptiere ein Scratch- oder
Puffer-Argument oder halte es in einer Struktur, die der Aufrufer behält, anstatt pro Aufruf
zu allozieren. Lasse Aufrufer den anfänglichen Puffer bereitstellen, damit er auf dem Stack
leben oder aus einem Pool kommen kann:
var buf [8 << 10]byte; d := NewDecoderBuffer(r, buf[:]). - Verstecke die Pufferverwaltung hinter wenigen Methoden.
io.Readerkopiert in einen vom Aufrufer verwalteten Puffer und überlässt das Nachfüllen jedem Aufrufer. Eine kleine Fenster-API hält Daten an Ort und Stelle:Window() []bytezum Ansehen,Release(n)zum Konsumieren,Extend() intzum weiteren Lesen, mit "das Fenster wird durch Release oder Extend ungültig" im Dokumentationskommentar. - Definiere Fehler aus der heißen Schleife heraus. Verwende Iteration, die einen booleschen
Wert oder ein leeres Ergebnis zurückgibt und den Fehler danach meldet:
for s.Next() { ... }danns.Err(). Verwende Sentinel-Ergebnisse auf dem langsamen Pfad (Extendgibt 0 zurück, dann fragt der Aufrufer nach dem Fehler), so dass die innere Schleife keine Fehlerverzweigungen hat. Behandleio.EOFwie jede andere Endbedingung. - Zähle die Arbeit pro Eingabeeinheit. Verarbeite jedes Byte einmal, mache Funktionsaufrufe pro Token oder Datensatz anstatt pro Byte und entferne Zwischenkopien. Der Wechsel von Pro-Byte- zu Pro-Token-Aufrufen ist oft der zweitgrößte Gewinn nach dem Entfernen von Allokationen.
- Schichte die API. Biete einen niedrigstufigen, allokationsfreien Baustein (rohe Tokens, Ansichten, Iteratoren mit eigenem Scratch) und baue die bequeme API darauf auf. Mache die bequeme, allokierende API nicht zur einzigen.
- Schreibe Eigentums- und Lebensdauer in Dokumentationskommentare: was geliehen, was behalten, was für gleichzeitige Nutzung sicher ist, was der Nullwert bedeutet.
- Mache den Nullwert nützlich, wo es natürlich ist.
- Bevorzuge unveränderliche, gemeinsam genutzte Werte und veröffentliche neue Schnappschüsse, anstatt Sperren um gemeinsam genutzten, veränderlichen Zustand, wenn Leser dominieren.
Messe die API, nicht nur die Implementierung: Vergleiche allocs/op und Durchsatz mit der API, die du ersetzt. references/examples.md zeigt diese Formen nebeneinander.
Optimierung einer heißen Schleife, zuletzt
Nur nachdem die API und die O-Notation stimmen und ein Profiling zeigt, dass die Schleife heiß ist:
- Ein
switchüber einen Zustand kompiliert zu einer Kette von Vergleichen. Kleine Methoden pro Zustand, ausgewählt durch einfunc-Feld (d.state = (*Decoder).stateValue), halten jeden Zustand lesbar und können diesen Dispatch entfernen. - Verschiebe eine Prüfung in die Funktion, die den Wert verwendet, damit der Compiler
Bereichsprüfungen als unnötig beweisen kann, und halte den Einstiegspunkt unter dem
Inlining-Budget (80; ein Aufruf kostet etwa 52). Überprüfe mit
go build -gcflags=-m=2. - Das Zusammenlegen zweier Funktionen, um einen Aufruf zu vermeiden, kann auf einem Pro-Token-Pfad 5-15% bringen, auf Kosten einer tieferen Verschachtelung. Mache das nur mit einem Benchmark und erkläre warum in einem Kommentar.
Wie Cheney sagt: Greife nicht zu diesen Tricks, "ohne die Big-O-Effekte in deiner API zu behandeln. Es wird die Nadel nicht bewegen."
Halte Funktionen klein und flach
Strebe an, dass jede Funktion eine Sache tut, mit höchstens ein oder zwei Verzweigungen. Als
Richtlinie halte die zyklomatische Komplexität bei oder unter 10 und die kognitive Komplexität
bei oder unter 10 (gocyclo, gocognit). Ein flacher Typ-Switch, der jeden Fall an einen Aufruf
übergibt, ist darüber in Ordnung; Verschachtelung nicht.
- Benenne mehrteilige Bedingungen. Ersetze
if len(a)*len(b) > maxN || size(a)*len(b)+size(b)*len(a) > maxMdurchif !fitsProduct(a, b). Der Name dokumentiert die Absicht und die Funktion kann die Erklärung tragen. - Dispatchen, dann delegieren. Ein Switch über Knoten- oder Nachrichtenarten sollte eine Methode pro Fall aufrufen, nicht die Logik eines jeden Falls enthalten.
- Früh zurückkehren. Zuerst Schutzklauseln, den Hauptpfad nicht eingerückt.
- Wiederhole keinen Fall wiederhole. Wenn sich zwei Zweige nur in der Operandenreihenfolge unterscheiden, schreibe einen Helfer und rufe ihn zweimal auf.
- Trenne gemischte Belange. "Finde es", "baue den neuen Wert" und "räume auf" sind drei Funktionen, nicht eine.
- Vermeide boolesche Flag-Parameter. Wenn einer die klarste Option ist, gib ihm einen aussagekräftigen Namen und erkläre ihn an der Aufrufstelle.
- Verwende Nachschlagetabellen (
map[string]Kind,[256]bool) anstatt Ketten von String- oder Byte-Vergleichen.
Gestalte Daten für den Leser und die CPU
- Gruppiere parallele Slices in ein Slice von Strukturen. Vier Slices, die mit derselben ID indiziert werden, sind eine Struktur. Es liest sich besser und verbessert normalerweise die Lokalität.
- Gib einem wiederholten Mechanismus seinen eigenen kleinen Typen. Wenn dieselbe Zähler-Umbruch-, Bitmap- oder Pufferlogik zweimal auftaucht, ist es ein Typ mit zwei oder drei Methoden.
- Bevorzuge explizite Felder gegenüber Bit-Packing, es sei denn, das Packen wird als
relevant gemessen.
[]segment{name, each}schlägt eineuint64mit Flags und Schiebe-Arithmetik. - Halte Repräsentationen gering. Weniger Zustände oder Arten bedeuten weniger doppelte Switches. Verschmelze einen Spezialfall mit dem allgemeinen, wenn es nichts messbares kostet.
Belege Leistungsbehauptungen
Behaupte niemals, eine Änderung sei schneller, langsamer oder allokationsfrei, ohne sie zu messen. Bevor du performance-sensitiven Code änderst:
- Speichere eine
benchstat-Basislinie der relevanten Benchmarks. - Mache die Änderung, dann vergleiche. Berichte signifikante Unterschiede mit Zahlen.
- Vergleiche die Ausgabe von
go build -gcflags=-mmit der Basis für neue Heap-Escapes. - Behalte oder füge
testing.AllocsPerRun-Tests für Pfade hinzu, die null Allokationen versprechen. - Erstelle ein Profil, bevor du über die Ursache einer Regression rätst.
Wenn Vereinfachen messbare heiße Pfadzeit kostet und du sie nicht sauber zurückgewinnen kannst, stoppe und berichte den Kompromiss, anstatt leise Komplexität wieder hinzuzufügen oder leise die Regression zu akzeptieren.
Lies references/verify.md für die genauen Befehle, einschließlich der Messung der Komplexität und der Fallstricke, die häufig Regressionen verursachen. Lies references/examples.md für Vorher-Nachher-Umgestaltungen, die den Stil zeigen.
Folge Go-Konventionen
gofmt,go vetund der Linter des Projekts müssen bestehen. Korrigiere Code, schwäche oder schließe Linter-Regeln nicht aus; unterdrücke eine einzelne Zeile nur mit einer benannten Regel und Begründung.- Kurze Empfängernamen, konsistent über die Methoden eines Typs; mische keine Zeiger- und Wert-Empfänger an einem Typ ohne Grund.
- Dokumentationskommentare beginnen mit dem Namen des Identifikators und geben den Vertrag an.
- Kommentiere das Warum, nicht das Was. Passe die umgebende Kommentardichte an.
- Standardbibliothek zuerst (
slices,maps,strings,math/bits) vor selbst geschriebenen Helfern. - Bearbeite generierten Code niemals von Hand.
Beim Überprüfen oder Umgestalten
- Identifiziere den heißen Pfad und seine Benchmarks, bevor du etwas anfasst.
- Messe die Komplexität, um die schlimmsten Funktionen zu finden; korrigiere diese zuerst.
- Gestalte in kleinen Schritten um und führe nach jedem Schritt fokussierte Tests aus.
- Behalte das Verhalten identisch: gleiche Ergebnisse, gleiche Budgets und Fallbacks, gleiche Allokationsgarantien. Ändere das Verhalten nur als separate, klar kommunizierte Entscheidung.
- Berichte, was sich geändert hat, die Vorher-/Nachher-Komplexität und Benchmark-Zahlen sowie alles, was nicht verifiziert wurde.