Bei Protocol Buffers (kurz protobuf) handelt es sich um ein von Google entwickelter Mechanismus mit dem man sprach- und plattformneutral strukturierte Daten serialisieren kann. Google spricht davon, dass man es mit XML vergleichen kann, allerdings bezeichnen sie Protocol Buffers als kleiner, schneller und einfacher.
Google’s data interchange format
Ausgangspunkt für die Arbeit mit protobuf ist eine .proto Datei (eine IDL - Interface Definition Language). Diese wird mit Hilfe eines Compilers in Source-Code für die gewünschte Programmiersprache übersetzt.
Anders als bei anderen Datenformaten ermöglicht es protobuf die Schnittstellenbeschreibung nachträglich anzupassen ohne sämtliche Clients, die gegen die alte Version compiliert wurden, zu “brechen”.
Weitere Informationen zu Protocol Buffers findet man auf der Projektseite bei Google.
Wie oben bereits beschrieben wird ein Compiler benötigt, mit dem man die .proto
Dateien in Sourcecode überführen kann. Dieser kann entweder als Package über eine entsprechende Packetverwaltung wie apt oder direkt als Binary von der ProtoBuf GitHub-Seite installiert werden.
Zum Beispiel:
$ sudo apt-get install protobuf-compiler
Zusätzlich wird ein sprachabhängiges Plugin benötigt, mit dem der eigentliche Sourcecode erzeugt wird. Ansonsten droht folgender Fehler wenn man Quellcode für Go erzeugen möchte:
$ protoc --go_out=. *.proto
protoc-gen-go: program not found or is not executable
--go_out: protoc-gen-go: Plugin failed with status code 1.
Für Go findet man ein protobuf Plugin im GitHub. Installiert werden kann es mittels:
$ go get -u github.com/golang/protobuf/protoc-gen-go
Durch den go get
Aufruf wird das Plugin unter $GOPATH/bin
bzw. $GOBIN
installiert. Das sollte dazu führen, dass das Plugin im $PATH
liegt und vom Compiler gefunden werden kann.
Wichtig ist sicherzustellen, dass das Binary im PATH liegt und zum Compiler gefunden werden kann!
Jetzt ist das Tooling bereit und es kann richtig losgehen.
Im Beispiel soll ein einfacher Datentyp HelloProto
serialisiert werden (Datei hello.proto
). Er besteht aus zwei Feldern message
und important
. Jedem Feld muss ein Datentyp sowie eine eindeutige ID zugewiesen werden.
hello.proto
syntax = "proto3";
package main;
message HelloProto {
string message = 1;
bool important = 2;
}
Die angegebenen Datentypen können entweder skalar oder zusammengesetzt sein. Mit Hilfe der eindeutigen ID wird das Feld im Binärdatenstorm identifiziert und sollte nicht mehr geändert werden. Neue IDs können später ohne Probleme hinzugefügt werden. Im obigen Beispiel erhält das Attribut message
die ID 1
und important
die ID 2
.
Der Go-Quelllcode kann nun mit folgendem Kommando erzeugt werden:
$ protoc --go_out=. *.proto
Daraus entsteht die Datei hello.pb.go
mit einem type
Hello World
und weiteren Hilfsmethoden.
type HelloWorld struct {
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
Important bool `protobuf:"varint,2,opt,name=important,proto3" json:"important,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
Zum Serialisieren der Datentypen gibt es im github.com/golang/protobuf/proto
Package die Funktionen Marshal()
bzw. Unmarshal()
. Im Beispiel hier wird ein neues Objekt in eine Datei geschrieben und anschließend wieder ausgelesen und ausgegeben.
Ausschnitt aus main.go
hello := &HelloWorld{Message: "Hello World"}
fmt.Println(proto.MarshalTextString(hello))
b, err := proto.Marshal(hello)
if err != nil {
return fmt.Errorf("could not encode hello %v", err)
}
f, err := os.OpenFile("db.blob", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return fmt.Errorf("could not open db %v", err)
}
defer f.Close()
_, err = f.Write(b)
if err != nil {
return fmt.Errorf("could not write to %v", err)
}
b, err = ioutil.ReadFile("db.blob")
if err != nil {
return fmt.Errorf("could not read file %v", err)
}
newWorld := &HelloWorld{}
proto.Unmarshal(b, newWorld)
fmt.Println(newWorld.Message)
Das komplette Beispiel kann auch in GitHub gefunden werden.
package main
import (
"fmt"
"io/ioutil"
"os"
"github.com/golang/protobuf/proto"
)
func main() {
hello := &HelloWorld{Message: "Hello World"}
write(hello)
}
func write(hello *HelloWorld) error {
fmt.Println(proto.MarshalTextString(hello))
b, err := proto.Marshal(hello)
if err != nil {
return fmt.Errorf("could not encode hello %v", err)
}
f, err := os.OpenFile("db.blob", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return fmt.Errorf("could not open db %v", err)
}
defer f.Close()
_, err = f.Write(b)
if err != nil {
return fmt.Errorf("could not write to %v", err)
}
b, err = ioutil.ReadFile("db.blob")
if err != nil {
return fmt.Errorf("could not read file %v", err)
}
newWorld := &HelloWorld{}
proto.Unmarshal(b, newWorld)
fmt.Println(newWorld.Message)
return nil
}
Ein weiterer Aspekt, der in diesem Artikel nicht angesprochen wurde, ist die Definition von Services. Auch das ist möglich. Darüber lassen sich RPC basierte Systeme beschreiben und auch generieren. Eine Anwendung ist das gRPC Protokoll was in einem weiteren Artikel besprochen wird.
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
22.02.2019
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com