Go Generated Code Guide (Open)

Beschreibt genau, welcher Go-Code vom Protocol Buffer Compiler für jede gegebene Protokolldefinition generiert wird.

Alle Unterschiede zwischen proto2, proto3 und Editions generiertem Code werden hervorgehoben – beachten Sie, dass diese Unterschiede im generierten Code liegen, wie in diesem Dokument beschrieben, und nicht in der Basis-API, die in beiden Versionen gleich ist. Sie sollten den proto2 Sprachleitfaden, proto3 Sprachleitfaden oder Editions Sprachleitfaden lesen, bevor Sie dieses Dokument lesen.

Compiler-Aufruf

Der Protocol Buffer Compiler benötigt ein Plugin, um Go-Code zu generieren. Installieren Sie es mit Go 1.16 oder höher, indem Sie Folgendes ausführen:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

Dadurch wird eine protoc-gen-go Binärdatei in $GOBIN installiert. Setzen Sie die Umgebungsvariable $GOBIN, um den Installationsort zu ändern. Sie muss in Ihrem $PATH enthalten sein, damit der Protocol Buffer Compiler sie findet.

Der Protocol Buffer Compiler gibt Go-Ausgabe aus, wenn er mit dem Flag go_out aufgerufen wird. Das Argument für das Flag go_out ist das Verzeichnis, in das der Compiler Ihre Go-Ausgabe schreiben soll. Der Compiler erstellt eine einzelne Quelldatei für jede .proto-Eingabedatei. Der Name der Ausgabedatei wird erstellt, indem die .proto-Erweiterung durch .pb.go ersetzt wird.

Wo in welchem Verzeichnis die generierte .pb.go-Datei platziert wird, hängt von den Compiler-Flags ab. Es gibt mehrere Ausgabemodi:

  • Wenn das Flag paths=import angegeben ist, wird die Ausgabedatei in ein Verzeichnis gelegt, das nach dem Importpfad des Go-Pakets benannt ist (wie z. B. dem durch die Option go_package innerhalb der .proto-Datei bereitgestellten). Zum Beispiel führt eine Eingabedatei protos/buzz.proto mit einem Go-Importpfad von example.com/project/protos/fizz zu einer Ausgabedatei unter example.com/project/protos/fizz/buzz.pb.go. Dies ist der Standard-Ausgabemodus, wenn kein paths-Flag angegeben ist.
  • Wenn das Flag module=$PREFIX angegeben ist, wird die Ausgabedatei in ein Verzeichnis gelegt, das nach dem Importpfad des Go-Pakets benannt ist (wie z. B. dem durch die Option go_package innerhalb der .proto-Datei bereitgestellten), wobei das angegebene Verzeichnispräfix aus dem Namen der Ausgabedatei entfernt wird. Zum Beispiel führt eine Eingabedatei protos/buzz.proto mit einem Go-Importpfad von example.com/project/protos/fizz und example.com/project als module-Präfix zu einer Ausgabedatei unter protos/fizz/buzz.pb.go. Die Generierung von Go-Paketen außerhalb des Modulpfads führt zu einem Fehler. Dieser Modus ist nützlich, um generierte Dateien direkt in ein Go-Modul auszugeben.
  • Wenn das Flag paths=source_relative angegeben ist, wird die Ausgabedatei im gleichen relativen Verzeichnis wie die Eingabedatei platziert. Zum Beispiel führt eine Eingabedatei protos/buzz.proto zu einer Ausgabedatei unter protos/buzz.pb.go.

Flags, die spezifisch für protoc-gen-go sind, werden durch Übergabe eines go_opt-Flags bei der Invokation von protoc bereitgestellt. Mehrere go_opt-Flags können übergeben werden. Zum Beispiel bei der Ausführung von

protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto

liest der Compiler die Eingabedateien foo.proto und bar/baz.proto aus dem Verzeichnis src und schreibt die Ausgabedateien foo.pb.go und bar/baz.pb.go in das Verzeichnis out. Der Compiler erstellt automatisch verschachtelte Ausgabeverzeichnisse, falls erforderlich, erstellt jedoch nicht das Ausgabeverzeichnis selbst.

Pakete (Packages)

Um Go-Code zu generieren, muss der Importpfad des Go-Pakets für jede .proto-Datei angegeben werden (einschließlich derer, von denen die zu generierenden .proto-Dateien transitiv abhängen). Es gibt zwei Möglichkeiten, den Go-Importpfad anzugeben:

  • indem er in der .proto-Datei deklariert wird, oder
  • indem er auf der Kommandozeile bei der Invokation von protoc deklariert wird.

Wir empfehlen, ihn in der .proto-Datei zu deklarieren, damit die Go-Pakete für .proto-Dateien zentral mit den .proto-Dateien identifiziert werden können und um die Menge der beim Aufruf von protoc zu übergebenden Flags zu vereinfachen. Wenn der Go-Importpfad für eine gegebene .proto-Datei sowohl in der .proto-Datei selbst als auch auf der Kommandozeile angegeben ist, hat letztere Vorrang.

Der Go-Importpfad wird lokal in einer .proto-Datei durch Deklaration einer Option go_package mit dem vollständigen Importpfad des Go-Pakets angegeben. Beispielverwendung:

option go_package = "example.com/project/protos/fizz";

Der Go-Importpfad kann bei der Invokation des Compilers auf der Kommandozeile angegeben werden, indem ein oder mehrere Flags M${PROTO_FILE}=${GO_IMPORT_PATH} übergeben werden. Beispielverwendung:

protoc --proto_path=src \
  --go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz \
  --go_opt=Mprotos/bar.proto=example.com/project/protos/foo \
  protos/buzz.proto protos/bar.proto

Da die Zuordnung aller .proto-Dateien zu ihren Go-Importpfaden sehr groß sein kann, wird diese Methode der Angabe der Go-Importpfade im Allgemeinen von einem Build-Tool (z. B. Bazel) durchgeführt, das die Kontrolle über den gesamten Abhängigkeitsbaum hat. Wenn es doppelte Einträge für eine gegebene .proto-Datei gibt, hat der zuletzt angegebene Vorrang.

Sowohl für die Option go_package als auch für das Flag M kann der Wert einen expliziten Paketnamen enthalten, der durch ein Semikolon vom Importpfad getrennt ist. Zum Beispiel: "example.com/protos/foo;package_name". Diese Verwendung wird nicht empfohlen, da der Paketname standardmäßig aus dem Importpfad auf vernünftige Weise abgeleitet wird.

Der Importpfad wird verwendet, um zu bestimmen, welche Importanweisungen generiert werden müssen, wenn eine .proto-Datei eine andere .proto-Datei importiert. Wenn z. B. a.proto b.proto importiert, muss die generierte Datei a.pb.go das Go-Paket importieren, das die generierte Datei b.pb.go enthält (sofern beide Dateien im selben Paket sind). Der Importpfad wird auch verwendet, um Ausgabedateinamen zu konstruieren. Weitere Details finden Sie im Abschnitt "Compiler Invocation" oben.

Es gibt keine Korrelation zwischen dem Go-Importpfad und dem package-Spezifizierer in der .proto-Datei. Letzterer ist nur für den Protokollpuffer-Namensraum relevant, während ersterer nur für den Go-Namensraum relevant ist. Ebenso gibt es keine Korrelation zwischen dem Go-Importpfad und dem .proto-Importpfad.

API-Level

Der generierte Code verwendet entweder die Open Struct API oder die Opaque API. Informationen zur Einführung finden Sie im Blogbeitrag Go Protobuf: The new Opaque API.

Abhängig von der Syntax, die Ihre .proto-Datei verwendet, ist hier die verwendete API:

.proto-SyntaxAPI-Level
proto2Open Struct API
proto3Open Struct API
Edition 2023Open Struct API
Edition 2024+Opaque API

Sie können die API auswählen, indem Sie die api_level Editions-Funktion in Ihrer .proto-Datei festlegen. Dies kann pro Datei oder pro Nachricht geschehen:

edition = "2023";

package log;

import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPAQUE;

message LogEntry {  }

Zu Ihrer Bequemlichkeit können Sie auch das Standard-API-Level mit einem protoc-Kommandozeilenflag überschreiben:

protoc […] --go_opt=default_api_level=API_HYBRID

Um das Standard-API-Level für eine bestimmte Datei (anstatt für alle Dateien) zu überschreiben, verwenden Sie das Mapping-Flag apilevelM (ähnlich wie das M-Flag für Importpfade):

protoc […] --go_opt=apilevelMhello.proto=API_HYBRID

Die Kommandozeilen-Flags funktionieren auch für .proto-Dateien, die noch proto2- oder proto3-Syntax verwenden. Wenn Sie jedoch das API-Level innerhalb der .proto-Datei auswählen möchten, müssen Sie diese Datei zuerst auf Editions migrieren.

Nachrichten

Angesichts einer einfachen Nachrichten Deklaration:

message Artist {}

der Protocol Buffer Compiler generiert eine Struktur namens Artist. Ein *Artist implementiert die proto.Message-Schnittstelle.

Das Paket proto stellt Funktionen bereit, die auf Nachrichten operieren, einschließlich der Konvertierung in und aus dem Binärformat.

Die Schnittstelle proto.Message definiert eine Methode ProtoReflect. Diese Methode gibt eine protoreflect.Message zurück, die eine reflexionsbasierte Ansicht der Nachricht liefert.

Die Option optimize_for hat keinen Einfluss auf die Ausgabe des Go-Code-Generators.

Wenn mehrere Goroutinen gleichzeitig auf dieselbe Nachricht zugreifen, gelten die folgenden Regeln:

  • Der gleichzeitige Zugriff (Lesen) auf Felder ist sicher, mit einer Ausnahme:
    • Der erste Zugriff auf ein lazy field ist eine Modifikation.
  • Das Modifizieren verschiedener Felder in derselben Nachricht ist sicher.
  • Das gleichzeitige Modifizieren eines Feldes ist nicht sicher.
  • Das gleichzeitige Modifizieren einer Nachricht auf irgendeine Weise mit Funktionen des Pakets proto, wie z. B. proto.Marshal oder proto.Size, ist nicht sicher.

Verschachtelte Typen

Eine Nachricht kann innerhalb einer anderen Nachricht deklariert werden. Zum Beispiel:

message Artist {
  message Name {
  }
}

In diesem Fall generiert der Compiler zwei Strukturen: Artist und Artist_Name.

Felder

Der Protocol Buffer Compiler generiert ein Strukturfeld für jedes Feld, das innerhalb einer Nachricht definiert ist. Die genaue Beschaffenheit dieses Feldes hängt von seinem Typ ab und davon, ob es sich um ein singuläres Feld, ein wiederholtes Feld, ein Map-Feld oder ein Oneof-Feld handelt.

Beachten Sie, dass die generierten Go-Feldnamen immer die Camel-Case-Namenskonvention verwenden, auch wenn der Feldname in der .proto-Datei kleingeschrieben mit Unterstrichen erfolgt (wie es sein sollte). Die Groß-/Kleinschreibungsumwandlung funktioniert wie folgt:

  1. Der erste Buchstabe wird für den Export großgeschrieben. Wenn das erste Zeichen ein Unterstrich ist, wird es entfernt und ein großes X vorangestellt.
  2. Wenn ein interner Unterstrich von einem Kleinbuchstaben gefolgt wird, wird der Unterstrich entfernt und der folgende Buchstabe großgeschrieben.

Somit wird aus dem Proto-Feld birth_year in Go BirthYear und aus _birth_year_2 wird XBirthYear_2.

Singuläre explizite Präsenz Skalarfelder

Für die Felddedefinition

int32 birth_year = 1;

generiert der Compiler eine Struktur mit einem Feld vom Typ *int32 namens BirthYear und einer Zugriffsermittlungsmethode GetBirthYear(), die den int32-Wert in Artist oder den Standardwert zurückgibt, wenn das Feld nicht gesetzt ist. Wenn der Standardwert nicht explizit gesetzt ist, wird stattdessen der Nullwert dieses Typs verwendet (0 für Zahlen, die leere Zeichenkette für Zeichenketten).

Für andere Skalarfelddatentypen (einschließlich bool, bytes und string) wird *int32 durch den entsprechenden Go-Datentyp gemäß der Tabelle der skalaren Werttypen ersetzt.

Singuläre implizite Präsenz Skalarfelder

Für diese Felddefinition:

int32 birth_year = 1;

Der Compiler generiert eine Struktur mit einem Feld vom Typ int32 namens BirthYear und einer Zugriffsermittlungsmethode GetBirthYear(), die den int32-Wert in birth_year oder den Nullwert dieses Typs zurückgibt, wenn das Feld nicht gesetzt ist (0 für Zahlen, die leere Zeichenkette für Zeichenketten).

Das Strukturfeld FirstActiveYear wird vom Typ *int32 sein, da es als optional markiert ist.

Für andere Skalarfelddatentypen (einschließlich bool, bytes und string) wird int32 durch den entsprechenden Go-Datentyp gemäß der Tabelle der skalaren Werttypen ersetzt. Nicht gesetzte Werte im Proto werden als Nullwert dieses Typs dargestellt (0 für Zahlen, die leere Zeichenkette für Zeichenketten).

Singuläre Nachrichtenfelder

Gegeben den Nachrichtentyp:

message Band {}

Für eine Nachricht mit einem Feld Band:

// proto2
message Concert {
  optional Band headliner = 1;
  // The generated code is the same result if required instead of optional.
}

// proto3
message Concert {
  Band headliner = 1;
}

// editions
message Concer {
  Band headliner = 1;
}

Generiert der Compiler eine Go-Struktur:

type Concert struct {
    Headliner *Band
}

Nachrichtenfelder können auf nil gesetzt werden, was bedeutet, dass das Feld nicht gesetzt ist und effektiv gelöscht wird. Dies ist nicht gleichbedeutend mit dem Setzen des Wertes auf eine "leere" Instanz der Nachrichtenstruktur.

Der Compiler generiert auch eine Hilfsfunktion func (m *Concert) GetHeadliner() *Band. Diese Funktion gibt nil für *Band zurück, wenn m nil ist oder headliner nicht gesetzt ist. Dies ermöglicht es, Get-Aufrufe ohne zwischengeschaltete nil-Prüfungen zu verketten.

var m *Concert // defaults to nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())

Wiederholte Felder

Jedes wiederholte Feld generiert ein Slice vom Typ T im Strukturfeld in Go, wobei T der Elementtyp des Feldes ist. Für diese Nachricht mit einem wiederholten Feld:

message Concert {
  // Best practice: use pluralized names for repeated fields:
  // /programming-guides/style#repeated-fields
  repeated Band support_acts = 1;
}

generiert der Compiler die Go-Struktur:

type Concert struct {
    SupportActs []*Band
}

Ebenso generiert der Compiler für die Felddedefinition repeated bytes band_promo_images = 1; eine Go-Struktur mit einem Feld vom Typ [][]byte namens BandPromoImage. Für eine wiederholte Aufzählung wie repeated MusicGenre genres = 2; generiert der Compiler eine Struktur mit einem Feld vom Typ []MusicGenre namens Genre.

Das folgende Beispiel zeigt, wie das Feld gesetzt wird:

concert := &Concert{
  SupportActs: []*Band{
    {}, // First element.
    {}, // Second element.
  },
}

Um auf das Feld zuzugreifen, können Sie Folgendes tun:

support := concert.GetSupportActs() // support type is []*Band.
b1 := support[0] // b1 type is *Band, the first element in support_acts.

Map-Felder

Jedes Map-Feld generiert ein Feld in der Struktur vom Typ map[TKey]TValue, wobei TKey der Schlüsseltyp des Feldes und TValue der Werttyp des Feldes ist. Für diese Nachricht mit einem Map-Feld:

message MerchItem {}

message MerchBooth {
  // items maps from merchandise item name ("Signed T-Shirt") to
  // a MerchItem message with more details about the item.
  map<string, MerchItem> items = 1;
}

generiert der Compiler die Go-Struktur:

type MerchBooth struct {
    Items map[string]*MerchItem
}

Oneof-Felder

Für ein Oneof-Feld generiert der Protocol Buffer Compiler ein einzelnes Feld mit einem Interface-Typ isMessageName_MyField. Außerdem generiert er für jedes der singulären Felder innerhalb des Oneof eine Struktur. Diese implementieren alle diese isMessageName_MyField-Schnittstelle.

Für diese Nachricht mit einem Oneof-Feld:

package account;
message Profile {
  oneof avatar {
    string image_url = 1;
    bytes image_data = 2;
  }
}

generiert der Compiler die Strukturen:

type Profile struct {
    // Types that are valid to be assigned to Avatar:
    //  *Profile_ImageUrl
    //  *Profile_ImageData
    Avatar isProfile_Avatar `protobuf_oneof:"avatar"`
}

type Profile_ImageUrl struct {
        ImageUrl string
}
type Profile_ImageData struct {
        ImageData []byte
}

Sowohl *Profile_ImageUrl als auch *Profile_ImageData implementieren isProfile_Avatar, indem sie eine leere Methode isProfile_Avatar() bereitstellen.

Das folgende Beispiel zeigt, wie das Feld gesetzt wird:

p1 := &account.Profile{
  Avatar: &account.Profile_ImageUrl{ImageUrl: "http://example.com/image.png"},
}

// imageData is []byte
imageData := getImageData()
p2 := &account.Profile{
  Avatar: &account.Profile_ImageData{ImageData: imageData},
}

Um auf das Feld zuzugreifen, können Sie eine Typumwandlung (type switch) auf den Wert anwenden, um die verschiedenen Nachrichtentypen zu behandeln.

switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
    // Load profile image based on URL
    // using x.ImageUrl
case *account.Profile_ImageData:
    // Load profile image based on bytes
    // using x.ImageData
case nil:
    // The field is not set.
default:
    return fmt.Errorf("Profile.Avatar has unexpected type %T", x)
}

Der Compiler generiert auch Get-Methoden func (m *Profile) GetImageUrl() string und func (m *Profile) GetImageData() []byte. Jede Get-Funktion gibt den Wert für dieses Feld zurück oder den Nullwert, wenn er nicht gesetzt ist.

Aufzählungen (Enums)

Gegeben eine Aufzählung wie:

message Venue {
  enum Kind {
    KIND_UNSPECIFIED = 0;
    KIND_CONCERT_HALL = 1;
    KIND_STADIUM = 2;
    KIND_BAR = 3;
    KIND_OPEN_AIR_FESTIVAL = 4;
  }
  Kind kind = 1;
  // ...
}

generiert der Protocol Buffer Compiler einen Typ und eine Reihe von Konstanten mit diesem Typ:

type Venue_Kind int32

const (
    Venue_KIND_UNSPECIFIED       Venue_Kind = 0
    Venue_KIND_CONCERT_HALL      Venue_Kind = 1
    Venue_KIND_STADIUM           Venue_Kind = 2
    Venue_KIND_BAR               Venue_Kind = 3
    Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)

Für Aufzählungen innerhalb einer Nachricht (wie die obige) beginnt der Typname mit dem Nachrichtennamen.

type Venue_Kind int32

Für eine Aufzählung auf Paketebene:

enum Genre {
  GENRE_UNSPECIFIED = 0;
  GENRE_ROCK = 1;
  GENRE_INDIE = 2;
  GENRE_DRUM_AND_BASS = 3;
  // ...
}

bleibt der Go-Typname unverändert vom Proto-Aufzählungsnamen:

type Genre int32

Dieser Typ hat eine Methode String(), die den Namen eines gegebenen Wertes zurückgibt.

Die Methode Enum() initialisiert frisch zugewiesenen Speicher mit einem gegebenen Wert und gibt den entsprechenden Zeiger zurück.

func (Genre) Enum() *Genre

Der Protocol Buffer Compiler generiert eine Konstante für jeden Wert in der Aufzählung. Für Aufzählungen innerhalb einer Nachricht beginnen die Konstanten mit dem Namen der umschließenden Nachricht:

const (
    Venue_KIND_UNSPECIFIED       Venue_Kind = 0
    Venue_KIND_CONCERT_HALL      Venue_Kind = 1
    Venue_KIND_STADIUM           Venue_Kind = 2
    Venue_KIND_BAR               Venue_Kind = 3
    Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)

Für eine Aufzählung auf Paketebene beginnen die Konstanten stattdessen mit dem Aufzählungsnamen:

const (
    Genre_GENRE_UNSPECIFIED   Genre = 0
    Genre_GENRE_ROCK          Genre = 1
    Genre_GENRE_INDIE         Genre = 2
    Genre_GENRE_DRUM_AND_BASS Genre = 3
)

Der Protocol Buffer Compiler generiert auch eine Zuordnung von ganzzahligen Werten zu den Zeichenkettennamen und eine Zuordnung von den Namen zu den Werten.

var Genre_name = map[int32]string{
    0: "GENRE_UNSPECIFIED",
    1: "GENRE_ROCK",
    2: "GENRE_INDIE",
    3: "GENRE_DRUM_AND_BASS",
}
var Genre_value = map[string]int32{
    "GENRE_UNSPECIFIED":   0,
    "GENRE_ROCK":          1,
    "GENRE_INDIE":         2,
    "GENRE_DRUM_AND_BASS": 3,
}

Beachten Sie, dass die .proto-Sprache erlaubt, dass mehrere Aufzählungsymbole denselben numerischen Wert haben. Symbole mit demselben numerischen Wert sind Synonyme. Diese werden in Go auf genau dieselbe Weise dargestellt, wobei mehrere Namen demselben numerischen Wert entsprechen. Die umgekehrte Zuordnung enthält einen einzigen Eintrag für den numerischen Wert zum Namen, der in der .proto-Datei zuerst erscheint.

Erweiterungen (Extensions)

Gegeben eine Erweiterungsdefinition

extend Concert {
  int32 promo_id = 123;
}

Der Protocol Buffer Compiler generiert einen protoreflect.ExtensionType-Wert namens E_Promo_id. Dieser Wert kann mit den Funktionen proto.GetExtension, proto.SetExtension, proto.HasExtension und proto.ClearExtension verwendet werden, um auf eine Erweiterung in einer Nachricht zuzugreifen. Die Funktionen GetExtension und SetExtension geben den Erweiterungswerttyp zurück bzw. akzeptieren ihn als interface{}-Wert.

Für singuläre Skalarerweiterungsfelder ist der Erweiterungswerttyp der entsprechende Go-Typ aus der Tabelle der skalaren Werttypen.

Für singuläre eingebettete Nachrichten-Erweiterungsfelder ist der Erweiterungswerttyp *M, wobei M der Nachrichtentyp des Feldes ist.

Für wiederholte Erweiterungsfelder ist der Erweiterungswerttyp ein Slice des singulären Typs.

Zum Beispiel, gegeben die folgende Definition:

extend Concert {
  int32 singular_int32 = 1;
  repeated bytes repeated_strings = 2;
  Band singular_message = 3;
}

Erweiterungswerte können wie folgt abgerufen werden:

m := &somepb.Concert{}
proto.SetExtension(m, extpb.E_SingularInt32, int32(1))
proto.SetExtension(m, extpb.E_RepeatedString, []string{"a", "b", "c"})
proto.SetExtension(m, extpb.E_SingularMessage, &extpb.Band{})

v1 := proto.GetExtension(m, extpb.E_SingularInt32).(int32)
v2 := proto.GetExtension(m, extpb.E_RepeatedString).([][]byte)
v3 := proto.GetExtension(m, extpb.E_SingularMessage).(*extpb.Band)

Erweiterungen können innerhalb eines anderen Typs verschachtelt deklariert werden. Ein gängiges Muster ist beispielsweise folgendes:

message Promo {
  extend Concert {
    int32 promo_id = 124;
  }
}

In diesem Fall heißt der ExtensionType-Wert E_Promo_Concert.

Services

Der Go-Code-Generator erzeugt standardmäßig keine Ausgabe für Dienste. Wenn Sie das gRPC-Plugin aktivieren (siehe die gRPC Go Quickstart-Anleitung), wird Code zur Unterstützung von gRPC generiert.