Aufbau von Dual-Purpose-APIs mit Go Protobuf und gRPC-Gateway
James Reed
Infrastructure Engineer · Leapcell

Einleitung
In der heutigen vernetzten Softwarelandschaft sind Microservice-Architekturen und vielfältige Client-Anwendungen die Norm. Dies stellt oft eine Herausforderung dar: Wie bauen wir APIs, die performant und typsicher für die interne Service-zu-Service-Kommunikation sind, aber auch zugänglich und idiomatisch für externe webbasierte Clients?
Traditionelle RESTful APIs mögen für externe Consumer ausreichend sein, aber ihnen fehlt oft die starke Typisierung und Effizienz, die für die Inter-Service-Kommunikation gewünscht wird. Umgekehrt ist reines gRPC in internen Szenarien hervorragend, aber nicht direkt von Webbrowsern nutzbar.
Dieser Artikel befasst sich mit einer leistungsstarken Lösung mit Go, Googles Protocol Buffers (Protobuf) und gRPC-Gateway, um eine einzige Quelle der Wahrheit für Ihre API-Definitionen zu erstellen, die sowohl interne gRPC-Consumer als auch externe RESTful-Clients gut bedient. Wir werden untersuchen, wie diese Kombination die Entwicklung vereinfacht, Redundanz reduziert und ein wartbareres und skalierbareres API-Ökosystem fördert.
Kernkonzepte
Bevor wir uns mit der Implementierung befassen, sollten wir die beteiligten Schlüsseltechnologien klar verstehen:
-
Protocol Buffers (Protobuf): Ein sprachunabhängiger, plattformunabhängiger, erweiterbarer Mechanismus zur Serialisierung strukturierter Daten. Protobuf definiert ein Schema für Ihre Daten mithilfe von
.proto
-Dateien und fungiert als Interface Definition Language (IDL). Aus diesem Schema generieren Compiler Code in verschiedenen Sprachen (wie Go) für das Marshaling und Unmarshaling von Daten. Seine binäre Serialisierung ist sehr effizient, was ihn ideal für die Hochleistungs-Kommunikation macht. -
gRPC: Ein leistungsstarkes, Open-Source-basiertes universelles RPC-Framework. Auf Protobuf aufgebaut, handhabt gRPC automatisch Serialisierung, Netzkommunikation und Methodenaufrufe. Es unterstützt Funktionen wie Streaming, Authentifizierung und Load Balancing, was es zu einer robusten Wahl für die Inter-Service-Kommunikation macht. Seine starke Typisierung, die von Protobuf abgeleitet ist, gewährleistet Compile-Time-Checks und zuverlässige Datenverträge.
-
gRPC-Gateway: Ein Plugin für den Protobuf-Compiler, das einen Reverse-Proxy-Server generiert. Dieser Proxy übersetzt HTTP/JSON-Anfragen in gRPC-Anfragen und leitet sie dann an Ihren eigentlichen gRPC-Dienst weiter. Er übersetzt auch die gRPC-Antworten zurück in HTTP/JSON-Antworten. Im Wesentlichen ermöglicht gRPC-Gateway, Ihre gRPC-Dienste als konventionelle RESTful-APIs verfügbar zu machen, sodass sie von Webbrowsern und anderen HTTP-Clients genutzt werden können, ohne dass spezielle gRPC-Client-Bibliotheken erforderlich sind.
Aufbau von Dual-Purpose-APIs
Die Kernidee hinter diesem Ansatz ist die einmalige Definition Ihrer API mithilfe von Protobuf. Diese einzelne Definition dient dann als Vertrag sowohl für Ihren gRPC-Dienst als auch für sein von gRPC-Gateway generiertes RESTful-Gegenstück.
1. Definieren Sie Ihre API mit Protobuf
Beginnen wir mit der Definition einer einfachen API für einen "Todo"-Dienst. Erstellen Sie eine Datei namens proto/todo/todo.proto
:
syntax = "proto3"; package todo; option go_package = "github.com/your/repo/gen/proto/go/todo"; import "google/api/annotations.proto"; service TodoService { rpc CreateTodo(CreateTodoRequest) returns (Todo) { option (google.api.http) = { post: "/v1/todos" body: "*" }; } rpc GetTodo(GetTodoRequest) returns (Todo) { option (google.api.http) = { get: "/v1/todos/{id}" }; } rpc ListTodos(ListTodosRequest) returns (ListTodosResponse) { option (google.api.http) = { get: "/v1/todos" }; } } message Todo { string id = 1; string title = 2; string description = 3; bool completed = 4; } message CreateTodoRequest { string title = 1; string description = 2; } message GetTodoRequest { string id = 1; } message ListTodosRequest { } message ListTodosResponse { repeated Todo todos = 1; }
Beachten Sie den Import von google/api/annotations.proto
und die option (google.api.http)
-Annotationen. Diese sind entscheidend für gRPC-Gateway. Sie definieren, wie Ihre gRPC-Methoden auf HTTP-Methoden, Pfade und Anfrage-/Antwortkörper abgebildet werden.
2. Generieren Sie Go-Code
Als Nächstes müssen Sie Ihre Protobuf-Definitionen in Go-Code kompilieren. Dazu benötigen Sie protoc
und die Go Protobuf- und gRPC-Plugins sowie das gRPC-Gateway-Plugin. Vorausgesetzt, Sie haben diese installiert, könnten Sie eine Makefile
oder ein Skript für die Kompilierung verwenden:
# Installieren Sie protoc-gen-go, protoc-gen-go-grpc, protoc-gen-grpc-gateway, protoc-gen-openapiv2 # go install google.golang.org/protobuf/cmd/protoc-gen-go@latest # go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest # go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest # go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest mkdir -p gen/proto/go/todo protoc -I. \ -I/usr/local/include \ -I$(go env GOPATH)/src \ -I$(go env GOPATH)/pkg/mod \ --go_out=paths=source_relative:gen/proto/go \ --go-grpc_out=paths=source_relative:gen/proto/go \ --grpc-gateway_out=paths=source_relative:gen/proto/go \ --openapiv2_out=gen/proto/openapiv2 \ proto/todo/todo.proto
Dieser Befehl generiert mehrere Dateien:
gen/proto/go/todo/todo.pb.go
: Enthält die Protobuf-Nachrichtenstrukturen und Hilfsfunktionen.gen/proto/go/todo/todo_grpc.pb.go
: Enthält die gRPC-Dienstschnittstelle und Client-/Server-Stubs.gen/proto/go/todo/todo.pb.gw.go
: Der gRPC-Gateway-Reverse-Proxy-Code.gen/proto/openapiv2/todo/todo.swagger.json
: OpenAPI/Swagger-Dokumentation (optional, aber sehr nützlich!).
3. Implementieren Sie den gRPC-Dienst
Implementieren Sie nun die eigentliche Geschäftslogik für Ihren TodoService
.
// main.go (vereinfacht zur Veranschaulichung) package main import ( "context" "log" "net" "net/http" "os" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "github.com/your/repo/gen/proto/go/todo" ) // todoService implementiert die gRPC TodoService-Schnittstelle type todoService struct { pb.UnimplementedTodoServiceServer // Einbetten für zukünftige Kompatibilität } func NewTodoService() *todoService { return &todoService{} } func (s *todoService) CreateTodo(ctx context.Context, req *pb.CreateTodoRequest) (*pb.Todo, error) { log.Printf("CreateTodo-Anfrage erhalten: %v", req) // In einer echten Anwendung würden Sie dies in einer Datenbank speichern return &pb.Todo{ Id: "id-generated-123", // Simulation der ID-Generierung Title: req.GetTitle(), Description: req.GetDescription(), Completed: false, }, nil } func (s *todoService) GetTodo(ctx context.Context, req *pb.GetTodoRequest) (*pb.Todo, error) { log.Printf("GetTodo-Anfrage erhalten: %v", req) // Simulation des Abrufs aus der DB if req.GetId() == "id-generated-123" { return &pb.Todo{ Id: "id-generated-123", Title: "My First Todo", Description: "This is a detailed description.", Completed: false, }, } return nil, grpc.Errorf(codes.NotFound, "Todo mit ID %s nicht gefunden", req.GetId()) } func (s *todoService) ListTodos(ctx context.Context, req *pb.ListTodosRequest) (*pb.ListTodosResponse, error) { log.Printf("ListTodos-Anfrage erhalten") // Simulation des Abrufs aller Todos return &pb.ListTodosResponse{ Todos: []*pb.Todo{ {Id: "id-generated-123", Title: "My First Todo", Description: "Description 1", Completed: false}, {Id: "id-456", Title: "Buy groceries", Description: "Milk, eggs, bread", Completed: true}, }, }, nil } func main() { grpcPort := ":8080" httpPort := ":8081" // gRPC-Server starten lis, err := net.Listen("tcp", grpcPort) if err != nil { log.Fatalf("failed to listen: %v", err) } s *grpc.Server s = grpc.NewServer() pb.RegisterTodoServiceServer(s, NewTodoService()) log.Printf("gRPC-Server lauscht auf %s", grpcPort) go func() { if err := s.Serve(lis); err != nil { log.Fatalf("Failed to serve gRPC: %v", err) } }() // gRPC-Gateway-Proxy starten ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} err = pb.RegisterTodoServiceHandlerFromEndpoint(ctx, mux, "localhost"+grpcPort, opts) if err != nil { log.Fatalf("Failed to register gateway: %v", err) } log.Printf("gRPC-Gateway-Server lauscht auf %s", httpPort) if err := http.ListenAndServe(httpPort, mux); err != nil { log.Fatalf("Failed to serve gRPC-Gateway: %v", err) } }
Diese main.go
richtet zwei Server ein:
- Ein gRPC-Server, der auf Port 8080 lauscht und unseren
TodoService
implementiert. - Ein gRPC-Gateway-Proxy-Server, der auf Port 8081 lauscht. Dieser Server nimmt HTTP/JSON-Anfragen entgegen und leitet sie an den gRPC-Server weiter.
4. Anwendungsszenarien
Mit dieser Einrichtung können Sie nun:
-
Interne Microservices: Andere Go-Microservices können direkt mit Ihrem
TodoService
über gRPC auflocalhost:8080
kommunizieren. Sie profitieren von starker Typisierung, effizienter binärer Serialisierung und den erweiterten Funktionen von gRPC.// Internes gRPC-Client-Beispiel package main import ( "context" "log" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "github.com/your/repo/gen/proto/go/todo" ) func main() { conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("konnte nicht verbinden: %v", err) } defer conn.Close() client := pb.NewTodoServiceClient(conn) res, err := client.CreateTodo(context.Background(), &pb.CreateTodoRequest{ Title: "Internes Todo", Description: "Erstellt über gRPC", }) if err != nil { log.Fatalf("todo konnte nicht erstellt werden: %v", err) } log.Printf("Erstelltes Todo (gRPC): %v", res) }
-
Externe Webclients: Webbrowser, mobile Apps oder andere externe Systeme können mit Ihrem Dienst über Standard-HTTP/JSON-Anfragen auf
localhost:8081
interagieren.# Beispiel mit curl für HTTP/JSON curl -X POST -H "Content-Type: application/json" \ -d '{"title": "Externes Todo", "description": "Erstellt über HTTP"}' \ http://localhost:8081/v1/todos # Ausgabe: # {"id":"id-generated-123","title":"Externes Todo","description":"Erstellt über HTTP","completed":false} curl http://localhost:8081/v1/todos/id-generated-123 # Ausgabe: # {"id":"id-generated-123","title":"My First Todo","description":"This is a detailed description.","completed":false} curl http://localhost:8081/v1/todos # Ausgabe: # {"todos":[{"id":"id-generated-123","title":"My First Todo","description":"Description 1","completed":false},{"id":"id-456","title":"Buy groceries","description":"Milk, eggs, bread","completed":true}]}
Fazit
Durch die Nutzung von Go, Protocol Buffers und gRPC-Gateway können Sie eine einzige, robuste API-Definition (.proto
-Datei) erstellen, die Boilerplate-Code sowohl für Hochleistungs-gRPC-Dienste als auch für universell zugängliche RESTful HTTP/JSON-Endpunkte generiert.
Dieser Ansatz reduziert Duplizierung erheblich, verbessert die Konsistenz über verschiedene Consumer-Typen hinweg und strafft die API-Entwicklung und -Wartung. Mit dieser leistungsstarken Kombination können Sie APIs erstellen, die sowohl für die interne Kommunikation hocheffizient als auch für externe Clients mühelos benutzerfreundlich sind, und so wirklich das Beste aus beiden Welten erreichen.