Grundlagen von Protocol Buffers: Go
Dieses Tutorial bietet eine grundlegende Einführung für Go-Programmierer zur Arbeit mit Protocol Buffers unter Verwendung der proto3-Version der Protocol Buffers-Sprache. Anhand eines einfachen Beispielanwendungsprojekts wird gezeigt, wie Sie
- Nachrichtenformate in einer
.proto-Datei definieren. - Den Protocol Buffer-Compiler verwenden.
- Die Go-Protocol-Buffer-API zum Schreiben und Lesen von Nachrichten verwenden.
Dies ist kein umfassender Leitfaden zur Verwendung von Protocol Buffers in Go. Für detailliertere Referenzinformationen siehe den Protocol Buffer Language Guide, die Go API-Referenz, den Go Generated Code Guide und den Encoding Reference.
Das Problemfeld
Das Beispiel, das wir verwenden werden, ist eine sehr einfache "Adressbuch"-Anwendung, die Kontaktdaten von Personen lesen und in eine Datei schreiben kann. Jede Person im Adressbuch hat einen Namen, eine ID, eine E-Mail-Adresse und eine Telefonnummer.
Wie serialisiert und ruft man strukturierte Daten wie diese ab? Es gibt mehrere Möglichkeiten, dieses Problem zu lösen
- Verwenden Sie gobs, um Go-Datenstrukturen zu serialisieren. Dies ist eine gute Lösung in einer Go-spezifischen Umgebung, funktioniert jedoch nicht gut, wenn Sie Daten mit Anwendungen austauschen müssen, die für andere Plattformen geschrieben wurden.
- Man kann sich einen Ad-hoc-Weg ausdenken, um die Datenpunkte in einen einzigen String zu kodieren – z. B. 4 ganze Zahlen als „12:3:-23:67“ kodieren. Dies ist ein einfacher und flexibler Ansatz, obwohl er das Schreiben von einmaligen Kodierungs- und Parsing-Codes erfordert und das Parsen einen geringen Laufzeitaufwand verursacht. Dies funktioniert am besten für die Kodierung sehr einfacher Daten.
- Serialisieren Sie die Daten in XML. Dieser Ansatz kann sehr attraktiv sein, da XML (sozusagen) menschenlesbar ist und es Bindungsbibliotheken für viele Sprachen gibt. Dies kann eine gute Wahl sein, wenn Sie Daten mit anderen Anwendungen/Projekten teilen möchten. XML ist jedoch bekanntermaßen speicherintensiv, und die Kodierung/Dekodierung kann Anwendungen eine enorme Leistungseinbuße bescheren. Außerdem ist die Navigation durch einen XML-DOM-Baum erheblich komplizierter als die normale Navigation durch einfache Felder in einer Klasse.
Protocol Buffers sind die flexible, effiziente, automatisierte Lösung, um genau dieses Problem zu lösen. Mit Protocol Buffers schreiben Sie eine .proto-Beschreibung der Datenstruktur, die Sie speichern möchten. Daraus erstellt der Protocol Buffer-Compiler eine Klasse, die die automatische Kodierung und das Parsen von Protocol Buffer-Daten in einem effizienten Binärformat implementiert. Die generierte Klasse bietet Getter und Setter für die Felder, die eine Protocol Buffer ausmachen, und kümmert sich um die Details des Lesens und Schreibens der Protocol Buffer als Einheit. Wichtig ist, dass das Protocol Buffer-Format die Idee unterstützt, das Format im Laufe der Zeit so zu erweitern, dass der Code Daten, die mit dem alten Format kodiert wurden, weiterhin lesen kann.
Wo man den Beispielcode findet
Unser Beispiel ist eine Reihe von Befehlszeilenanwendungen zur Verwaltung einer Adressbuch-Datendatei, die mit Protocol Buffers kodiert ist. Der Befehl add_person_go fügt einen neuen Eintrag zur Datendatei hinzu. Der Befehl list_people_go parst die Datendatei und gibt die Daten auf der Konsole aus.
Das vollständige Beispiel finden Sie im Beispielverzeichnis des GitHub-Repositorys.
Definieren Ihres Protokollformats
Um Ihre Adressbuchanwendung zu erstellen, müssen Sie mit einer .proto-Datei beginnen. Die Definitionen in einer .proto-Datei sind einfach: Sie fügen eine Nachricht für jede Datenstruktur hinzu, die Sie serialisieren möchten, und geben dann einen Namen und einen Typ für jedes Feld in der Nachricht an. In unserem Beispiel ist die .proto-Datei, die die Nachrichten definiert, addressbook.proto.
Die .proto-Datei beginnt mit einer Paketdeklaration, die hilft, Namenskonflikte zwischen verschiedenen Projekten zu vermeiden.
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
Die Option go_package definiert den Importpfad des Pakets, das den gesamten generierten Code für diese Datei enthält. Der Go-Paketname ist die letzte Pfadkomponente des Importpfads. Zum Beispiel wird unser Beispiel einen Paketnamen von "tutorialpb" verwenden.
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
Als Nächstes haben Sie Ihre Nachrichten-Definitionen. Eine Nachricht ist einfach eine Aggregation, die eine Reihe von typisierten Feldern enthält. Viele einfache Standarddatentypen sind als Feldtypen verfügbar, darunter bool, int32, float, double und string. Sie können Ihren Nachrichten auch weitere Struktur verleihen, indem Sie andere Nachrichtentypen als Feldtypen verwenden.
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
In dem obigen Beispiel enthält die Person-Nachricht PhoneNumber-Nachrichten, während die AddressBook-Nachricht Person-Nachrichten enthält. Sie können sogar Nachrichtentypen definieren, die in andere Nachrichten verschachtelt sind – wie Sie sehen, ist der PhoneNumber-Typ in Person definiert. Sie können auch enum-Typen definieren, wenn Sie möchten, dass eines Ihrer Felder einen Wert aus einer vordefinierten Liste hat – hier möchten Sie angeben, dass eine Telefonnummer entweder PHONE_TYPE_MOBILE, PHONE_TYPE_HOME oder PHONE_TYPE_WORK sein kann.
Die Markierungen " = 1", " = 2" bei jedem Element identifizieren den eindeutigen "Tag", den dieses Feld in der Binärkodierung verwendet. Tag-Nummern von 1-15 erfordern ein Byte weniger zur Kodierung als höhere Nummern. Daher können Sie diese Tags als Optimierung für häufig verwendete oder wiederholte Elemente verwenden und die Tags 16 und höher für weniger häufig verwendete optionale Elemente übrig lassen. Jedes Element in einem wiederholten Feld erfordert eine erneute Kodierung der Tag-Nummer. Wiederholte Felder sind daher besonders gute Kandidaten für diese Optimierung.
Wenn ein Feldwert nicht gesetzt ist, wird ein Standardwert verwendet: Null für numerische Typen, eine leere Zeichenfolge für Zeichenfolgen, false für boolesche Werte. Für eingebettete Nachrichten ist der Standardwert immer die "Standardinstanz" oder das "Prototyp" der Nachricht, bei der keine Felder gesetzt sind. Das Aufrufen des Accessors zum Abrufen des Werts eines Feldes, das nicht explizit gesetzt wurde, gibt immer den Standardwert dieses Feldes zurück.
Wenn ein Feld repeated ist, kann das Feld beliebig oft (einschließlich null Mal) wiederholt werden. Die Reihenfolge der wiederholten Werte wird in der Protocol Buffer beibehalten. Betrachten Sie wiederholte Felder als dynamisch dimensionierte Arrays.
Einen vollständigen Leitfaden zum Schreiben von .proto-Dateien – einschließlich aller möglichen Feldtypen – finden Sie im Protocol Buffer Language Guide. Suchen Sie jedoch nicht nach Einrichtungen, die der Klassenvererbung ähneln – Protocol Buffers tun das nicht.
Kompilieren Ihrer Protocol Buffers
Nachdem Sie nun eine .proto-Datei haben, müssen Sie als Nächstes die Klassen generieren, die Sie zum Lesen und Schreiben von AddressBook (und damit Person und PhoneNumber) Nachrichten benötigen. Dazu müssen Sie den Protocol Buffer-Compiler protoc auf Ihrer .proto-Datei ausführen.
Wenn Sie den Compiler noch nicht installiert haben, laden Sie das Paket herunter und befolgen Sie die Anweisungen in der README-Datei.
Führen Sie den folgenden Befehl aus, um das Go-Protocol-Buffers-Plugin zu installieren
go install google.golang.org/protobuf/cmd/protoc-gen-go@latestDas Compiler-Plugin
protoc-gen-gowird in$GOBINinstalliert, standardmäßig in$GOPATH/bin. Es muss in Ihrem$PATHvorhanden sein, damit der Protocol-Compilerprotoces finden kann.Führen Sie nun den Compiler aus und geben Sie das Quellverzeichnis (wo sich der Quellcode Ihrer Anwendung befindet – das aktuelle Verzeichnis wird verwendet, wenn Sie keinen Wert angeben), das Zielverzeichnis (wo der generierte Code gespeichert werden soll; oft dasselbe wie
$SRC_DIR) und den Pfad zu Ihrer.proto-Datei an. In diesem Fall würden Sie aufrufenprotoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.protoDa Sie Go-Code wünschen, verwenden Sie die Option
--go_out– ähnliche Optionen sind für andere unterstützte Sprachen vorhanden.
Dies generiert github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go in Ihrem angegebenen Zielverzeichnis.
Die Protocol Buffer API
Die Generierung von addressbook.pb.go liefert Ihnen die folgenden nützlichen Typen
- Eine
AddressBook-Struktur mit einemPeople-Feld. - Eine
Person-Struktur mit Feldern fürName,Id,EmailundPhones. - Eine
Person_PhoneNumber-Struktur mit Feldern fürNumberundType. - Der Typ
Person_PhoneTypeund ein für jeden Wert imPerson.PhoneType-Enum definierter Wert.
Sie können im Go Generated Code Guide mehr über die Details dessen erfahren, was genau generiert wird, aber für die meisten Zwecke können Sie diese als ganz normale Go-Typen behandeln.
Hier ist ein Beispiel aus den Unit-Tests des list_people-Befehls, wie Sie eine Instanz von Person erstellen könnten
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "jdoe@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.PhoneType_PHONE_TYPE_HOME},
},
}
Eine Nachricht schreiben
Der gesamte Zweck der Verwendung von Protocol Buffers ist die Serialisierung Ihrer Daten, damit sie anderswo geparst werden können. In Go verwenden Sie die proto-Bibliothek Marshal-Funktion, um Ihre Protocol-Buffer-Daten zu serialisieren. Ein Zeiger auf die struct einer Protocol-Buffer-Nachricht implementiert die proto.Message-Schnittstelle. Der Aufruf von proto.Marshal gibt den Protocol-Buffer zurück, kodiert in seinem Wire-Format. Zum Beispiel verwenden wir diese Funktion im add_person-Befehl
book := &pb.AddressBook{}
// ...
// Write the new address book back to disk.
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
Eine Nachricht lesen
Um eine kodierte Nachricht zu parsen, verwenden Sie die proto-Bibliothek Unmarshal-Funktion. Dieser Aufruf parst die Daten in in als Protocol Buffer und speichert das Ergebnis in book. Um die Datei im list_people-Befehl zu parsen, verwenden wir
// Read the existing address book.
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
Erweitern eines Protocol Buffers
Früher oder später, nachdem Sie den Code veröffentlicht haben, der Ihre Protocol Buffer verwendet, werden Sie zweifellos die Definition des Protocol Buffers „verbessern“ wollen. Wenn Sie möchten, dass Ihre neuen Buffer abwärtskompatibel sind und Ihre alten Buffer vorwärtskompatibel sind – und das wollen Sie fast sicherlich –, dann gibt es einige Regeln, die Sie befolgen müssen. In der neuen Version des Protocol Buffers
- Sie dürfen die Tag-Nummern vorhandener Felder nicht ändern.
- Sie dürfen Felder löschen.
- Sie dürfen neue Felder hinzufügen, müssen aber neue Tag-Nummern verwenden (d. h. Tag-Nummern, die in dieser Protocol Buffer noch nie verwendet wurden, auch nicht von gelöschten Feldern).
(Es gibt einige Ausnahmen zu diesen Regeln, aber sie werden selten verwendet.)
Wenn Sie diese Regeln befolgen, liest alter Code neue Nachrichten problemlos und ignoriert einfach alle neuen Felder. Für alten Code haben einzelne Felder, die gelöscht wurden, einfach ihren Standardwert, und gelöschte wiederholte Felder sind leer. Neuer Code liest ebenfalls alte Nachrichten transparent.
Beachten Sie jedoch, dass neue Felder in alten Nachrichten nicht vorhanden sind. Sie müssen also etwas Sinnvolles mit dem Standardwert tun. Ein typspezifischer Standardwert wird verwendet: für Zeichenfolgen ist der Standardwert die leere Zeichenfolge. Für boolesche Werte ist der Standardwert false. Für numerische Typen ist der Standardwert Null.