🪟 Windows TippsHP Announces an AMD-Powered Portable Workstation(16.09.2026 um 01:59 Uhr)
🔧 ProgrammierungWhat I Actually Learned Adding Asgardeo Login to a Side Project(16.09.2026 um 01:27 Uhr)
🔧 ProgrammierungAnanthika Unlocks Athena: SQL Wisdom Without a Single Server ⚡(16.09.2026 um 01:27 Uhr)
🔧 ProgrammierungAWS Transit Gateway (TGW)(16.09.2026 um 01:30 Uhr)
🔧 ProgrammierungBuilding Answer Lineage for Enterprise Data Agents(16.09.2026 um 01:45 Uhr)
🔧 ProgrammierungThe Hacker Who Lost $7.73M to a Bot — rsETH Safe Module Exploit(16.09.2026 um 01:49 Uhr)
🔧 ProgrammierungSummaries drift. Facts should not.(16.09.2026 um 01:59 Uhr)
🪟 Windows TippsHP Announces an AMD-Powered Portable Workstation(16.09.2026 um 01:59 Uhr)
🔧 ProgrammierungWhat I Actually Learned Adding Asgardeo Login to a Side Project(16.09.2026 um 01:27 Uhr)
🔧 ProgrammierungAnanthika Unlocks Athena: SQL Wisdom Without a Single Server ⚡(16.09.2026 um 01:27 Uhr)
🔧 ProgrammierungAWS Transit Gateway (TGW)(16.09.2026 um 01:30 Uhr)
🔧 ProgrammierungBuilding Answer Lineage for Enterprise Data Agents(16.09.2026 um 01:45 Uhr)
🔧 ProgrammierungThe Hacker Who Lost $7.73M to a Bot — rsETH Safe Module Exploit(16.09.2026 um 01:49 Uhr)
🔧 ProgrammierungSummaries drift. Facts should not.(16.09.2026 um 01:59 Uhr)

🔧 Programmierung 🕛 vor 3 Monaten 17 Min Lesezeit
0

Beyond REST: Architecting High-Performance Microservices with gRPC in Go

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

When scaling backend architecture, the communication layer between services eventually

becomes a bottleneck. While REST over HTTP/1.1 with JSON payloads is the undisputed

standard for public-facing web APIs, it introduces significant serialization overhead

and lacks strict, enforceable contracts for internal, service-to-service communication.



This is where gRPC shines. By leveraging HTTP/2 for multiplexed transport and Protocol

Buffers (Protobuf) for efficient binary serialization, gRPC provides low-latency,

strictly-typed communication well-suited to internal microservice architectures.



However, gRPC is not a silver bullet. It introduces operational complexity — particularly

around Layer 7 load balancing and debugging binary traffic. This guide strips away the

hype and walks through building a production-ready gRPC client and server in Go, complete

with middleware (interceptors) and server-side streaming.



To make the examples concrete and representative of real-world systems, we will build

a three-tier Pharmacy Inventory service:




CODE
┌────────────┐     gRPC      ┌──────────────┐     HTTP/JSON     ┌──────────────────┐
│ gRPC Client│ ────────────► │ gRPC Server │ ────────────────► │ REST Inventory │
│ (consumer) │ │ :50051 │ │ API :8080 │
└────────────┘ └──────────────┘ └──────────────────┘






This architecture is intentionally realistic: the gRPC layer provides a strict,

typed contract to internal consumers, while delegating actual data retrieval to a

REST backend — exactly as you would find in a system that wraps an existing REST

service or third-party API with a high-performance internal interface.







Prerequisites and Workspace Setup



Ensure your development environment is on a current Go release (1.22 or later, as the

examples use the enhanced net/http routing patterns introduced in that version). You

will also need the Protocol Buffers compiler (protoc) and its Go code-generation

plugins.



Install the required Go plugins:




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






Initialize your module:




CODE
mkdir grpc-inventory
cd grpc-inventory
go mod init github.com/murtuza/grpc-inventory
go get google.golang.org/grpc
go get google.golang.org/protobuf









Project Structure



Before writing any code, it helps to see the layout we are building toward:




CODE
grpc-inventory/
├── proto/
│ └── inventory.proto # Service contract (single source of truth)
├── proto/inventory/
│ ├── inventory.pb.go # Generated: Protobuf message types
│ └── inventory_grpc.pb.go # Generated: gRPC service interfaces
├── rest-api/
│ └── main.go # REST inventory data layer (:8080)
├── server/
│ └── main.go # gRPC server — calls the REST API (:50051)
├── client/
│ └── main.go # gRPC client — consumes the gRPC server
└── go.mod












Step 1: Defining the Strict Contract (Protobuf)



The foundation of any gRPC service is its interface definition. Protobuf acts as a

strict contract between client and server — if the implementations do not match the

.proto file, the code will not compile.



Create proto/inventory.proto:




CODE
syntax = "proto3";

package inventory;

option go_package = "github.com/murtuza/grpc-inventory/proto/inventory";

// The Pharmacy Inventory service definition.
service InventoryService {
// Unary RPC: Fetch a single product's stock level.
rpc CheckStock (StockRequest) returns (StockResponse);

// Server-Streaming RPC: Push all items currently running low on stock.
rpc StreamLowStock (EmptyRequest) returns (stream StockResponse);
}

message EmptyRequest {}

message StockRequest {
string sku = 1;
}

message StockResponse {
string sku = 1;
string medicine_name = 2;
int32 quantity = 3;
bool requires_prescription = 4;
}






Generate the Go code from the project root:




CODE
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/inventory.proto






This produces two files:





  • proto/inventory/inventory.pb.go — the generated message structs


  • proto/inventory/inventory_grpc.pb.go — the generated client stub and server interface
    > Tip: Commit generated .pb.go files to version control. This makes the schema
    > auditable in code review and removes the protoc toolchain from your CI build path.









Step 2: The REST Inventory API (Data Layer)



Before building the gRPC server, we need the data source it will call. This REST API

represents the inventory database layer — it could equally be an existing internal

service, a third-party API, or a microservice you do not own.



Create rest-api/main.go:




CODE
package main

import (
"encoding/json"
"log"
"net/http"
)

// StockItem mirrors the shape we will return from the REST API.
// The gRPC server will map these fields onto its Protobuf StockResponse.
type StockItem struct {
SKU string `json:"sku"`
MedicineName string `json:"medicine_name"`
Quantity int `json:"quantity"`
RequiresPrescription bool `json:"requires_prescription"`
}

// inventory is the in-memory data store for this service.
// In production this would be replaced by a database query.
var inventory = map[string]StockItem{
"MED-001": {SKU: "MED-001", MedicineName: "Amoxicillin 500mg", Quantity: 1450, RequiresPrescription: true},
"MED-045": {SKU: "MED-045", MedicineName: "Metformin 500mg", Quantity: 320, RequiresPrescription: true},
"MED-089": {SKU: "MED-089", MedicineName: "Ibuprofen 200mg", Quantity: 12, RequiresPrescription: false},
"MED-102": {SKU: "MED-102", MedicineName: "Lisinopril 10mg", Quantity: 5, RequiresPrescription: true},
"MED-201": {SKU: "MED-201", MedicineName: "Cetirizine 10mg", Quantity: 8, RequiresPrescription: false},
}

// lowStockThreshold defines what "low stock" means across the system.
const lowStockThreshold = 50

// writeJSON is a small helper that sets the Content-Type header and encodes
// the provided value as JSON. It writes a 500 if encoding fails.
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
http.Error(w, `{"error":"encoding failed"}`, http.StatusInternalServerError)
}
}

// getStock handles GET /api/inventory/{sku}.
// It returns the stock record for a single SKU or 404 if not found.
func getStock(w http.ResponseWriter, r *http.Request) {
sku := r.PathValue("sku")
item, ok := inventory[sku]
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "SKU not found"})
return
}
writeJSON(w, http.StatusOK, item)
}

// getLowStock handles GET /api/inventory/low-stock.
// It returns all items whose quantity falls below lowStockThreshold.
func getLowStock(w http.ResponseWriter, r *http.Request) {
var items []StockItem
for _, item := range inventory {
if item.Quantity < lowStockThreshold {
items = append(items, item)
}
}
writeJSON(w, http.StatusOK, items)
}

func main() {
mux := http.NewServeMux()

// Register the specific /low-stock route BEFORE the wildcard {sku} route.
// Go 1.22+ ServeMux resolves conflicts by specificity, but explicit ordering
// makes the intent clear to future maintainers.
mux.HandleFunc("GET /api/inventory/low-stock", getLowStock)
mux.HandleFunc("GET /api/inventory/{sku}", getStock)

log.Println("REST Inventory API listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}









Testing the REST API with curl



Start the REST API in one terminal:




CODE
go run rest-api/main.go






Then verify each endpoint:



Look up a specific SKU:




CODE
curl -s http://localhost:8080/api/inventory/MED-001 | jq .









CODE
{
"sku": "MED-001",
"medicine_name": "Amoxicillin 500mg",
"quantity": 1450,
"requires_prescription": true
}






Fetch all low-stock items (quantity < 50):




CODE
curl -s http://localhost:8080/api/inventory/low-stock | jq .









CODE
[
{
"sku": "MED-089",
"medicine_name": "Ibuprofen 200mg",
"quantity": 12,
"requires_prescription": false
},
{
"sku": "MED-102",
"medicine_name": "Lisinopril 10mg",
"quantity": 5,
"requires_prescription": true
},
{
"sku": "MED-201",
"medicine_name": "Cetirizine 10mg",
"quantity": 8,
"requires_prescription": false
}
]






Confirm a 404 for an unknown SKU:




CODE
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/inventory/UNKNOWN
# 404






With the data layer verified independently, we can build the gRPC server with

confidence about the shape and behaviour of the API it will consume.







Step 3: The gRPC Server (Calls the REST API)



The gRPC server fulfils the InventoryService contract defined in Protobuf. Rather

than owning any data itself, it translates each incoming gRPC call into a corresponding

HTTP request to the REST API, then maps the JSON response onto a Protobuf message.



A shared http.Client with a connection timeout is created once and reused across all

requests. This is critical — constructing a new http.Client per request bypasses

Go's built-in connection pooling and introduces unnecessary overhead.



Create server/main.go:




CODE
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"time"

pb "github.com/murtuza/grpc-inventory/proto/inventory"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// restInventoryItem maps the JSON shape returned by the REST API.
type restInventoryItem struct {
SKU string `json:"sku"`
MedicineName string `json:"medicine_name"`
Quantity int `json:"quantity"`
RequiresPrescription bool `json:"requires_prescription"`
}

// server implements the InventoryServiceServer interface.
// Embedding UnimplementedInventoryServiceServer ensures forward compatibility —
// new RPC methods added to the .proto will not break existing server binaries.
type server struct {
pb.UnimplementedInventoryServiceServer
httpClient *http.Client
apiBaseURL string
}

// newServer constructs a server with a shared HTTP client.
// The REST API base URL is read from the REST_API_URL environment variable,
// defaulting to localhost for local development.
func newServer() *server {
baseURL := os.Getenv("REST_API_URL")
if baseURL == "" {
baseURL = "http://localhost:8080"
}
return &server{
httpClient: &http.Client{Timeout: 10 * time.Second},
apiBaseURL: baseURL,
}
}

// fetchItem calls GET /api/inventory/{sku} on the REST API and returns the parsed item.
// It maps HTTP 404 to a gRPC NotFound status so the gRPC client receives a
// well-typed error rather than a raw HTTP error code.
func (s *server) fetchItem(ctx context.Context, sku string) (*restInventoryItem, error) {
url := fmt.Sprintf("%s/api/inventory/%s", s.apiBaseURL, sku)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to build request: %v", err)
}

resp, err := s.httpClient.Do(req)
if err != nil {
return nil, status.Errorf(codes.Unavailable, "REST API unreachable: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
return nil, status.Errorf(codes.NotFound, "SKU %s not found in inventory", sku)
}
if resp.StatusCode != http.StatusOK {
return nil, status.Errorf(codes.Internal, "REST API returned unexpected status %d", resp.StatusCode)
}

var item restInventoryItem
if err := json.NewDecoder(resp.Body).Decode(&item); err != nil {
return nil, status.Errorf(codes.Internal, "failed to decode REST response: %v", err)
}
return &item, nil
}

// fetchLowStock calls GET /api/inventory/low-stock and returns all matching items.
func (s *server) fetchLowStock(ctx context.Context) ([]restInventoryItem, error) {
url := fmt.Sprintf("%s/api/inventory/low-stock", s.apiBaseURL)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to build request: %v", err)
}

resp, err := s.httpClient.Do(req)
if err != nil {
return nil, status.Errorf(codes.Unavailable, "REST API unreachable: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, status.Errorf(codes.Internal, "REST API returned unexpected status %d", resp.StatusCode)
}

var items []restInventoryItem
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
return nil, status.Errorf(codes.Internal, "failed to decode REST response: %v", err)
}
return items, nil
}

// CheckStock is a Unary RPC. It fetches a single SKU from the REST API and
// returns the result as a typed Protobuf message.
func (s *server) CheckStock(ctx context.Context, req *pb.StockRequest) (*pb.StockResponse, error) {
item, err := s.fetchItem(ctx, req.GetSku())
if err != nil {
return nil, err // already a gRPC status error
}
return &pb.StockResponse{
Sku: item.SKU,
MedicineName: item.MedicineName,
Quantity: int32(item.Quantity),
RequiresPrescription: item.RequiresPrescription,
}, nil
}

// StreamLowStock is a Server-Streaming RPC. It fetches all low-stock items from
// the REST API in a single call, then streams each one to the client individually.
func (s *server) StreamLowStock(_ *pb.EmptyRequest, stream pb.InventoryService_StreamLowStockServer) error {
items, err := s.fetchLowStock(stream.Context())
if err != nil {
return err
}

for _, item := range items {
// Respect client cancellation between sends.
if err := stream.Context().Err(); err != nil {
return status.Errorf(codes.Canceled, "client disconnected: %v", err)
}

if err := stream.Send(&pb.StockResponse{
Sku: item.SKU,
MedicineName: item.MedicineName,
Quantity: int32(item.Quantity),
RequiresPrescription: item.RequiresPrescription,
}); err != nil {
return err
}
}
return nil
}

// loggingInterceptor is Unary middleware that records the method, duration,
// and outcome of every incoming RPC call.
func loggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
h, err := handler(ctx, req)
log.Printf("[RPC] method=%s duration=%s err=%v", info.FullMethod, time.Since(start), err)
return h, err
}

func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// ChainUnaryInterceptor composes multiple interceptors cleanly.
// Add authInterceptor, recoveryInterceptor, etc. here as the service grows.
s := grpc.NewServer(
grpc.ChainUnaryInterceptor(loggingInterceptor),
)

pb.RegisterInventoryServiceServer(s, newServer())

log.Printf("Inventory gRPC server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}












Step 4: The gRPC Client



The client connects to the gRPC server and exercises both RPC patterns — a unary call

and a server-streaming call.



The traditional grpc.Dial is deprecated in modern versions of grpc-go. Its

replacement, grpc.NewClient, provides a more robust API and establishes the underlying

connection lazily on the first RPC call rather than at construction time.



Create client/main.go:




CODE
package main

import (
"context"
"io"
"log"
"time"

pb "github.com/murtuza/grpc-inventory/proto/inventory"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

func main() {
// grpc.NewClient does not block — the connection is established lazily.
// For production, replace insecure.NewCredentials() with a TLS config.
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("could not create client: %v", err)
}
defer conn.Close()

c := pb.NewInventoryServiceClient(conn)

testUnaryCall(c)
testStreamingCall(c)
}

func testUnaryCall(c pb.InventoryServiceClient) {
// A deadline context is essential for unary calls. Without one, a slow or
// unresponsive server will block the caller indefinitely.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

log.Println("--- Executing Unary RPC ---")
r, err := c.CheckStock(ctx, &pb.StockRequest{Sku: "MED-001"})
if err != nil {
log.Fatalf("CheckStock failed: %v", err)
}
log.Printf("Stock check -> %s: %d units (prescription required: %t)",
r.GetMedicineName(), r.GetQuantity(), r.GetRequiresPrescription())
}

func testStreamingCall(c pb.InventoryServiceClient) {
log.Println("\n--- Executing Server-Streaming RPC ---")

// A deadline is equally important on streaming calls. Use context.WithCancel
// if you need to abort the stream from the client side based on your own logic.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

stream, err := c.StreamLowStock(ctx, &pb.EmptyRequest{})
if err != nil {
log.Fatalf("StreamLowStock failed: %v", err)
}

for {
item, err := stream.Recv()
if err == io.EOF {
// The server closed the stream normally.
break
}
if err != nil {
log.Fatalf("error receiving from stream: %v", err)
}
log.Printf("ALERT: Low stock -> %s (SKU: %s) has only %d units remaining.",
item.GetMedicineName(), item.GetSku(), item.GetQuantity())
}
log.Println("--- Stream complete ---")
}












Step 5: Running the Full Stack



The three services run independently. Open three terminals from the project root.



Terminal 1 — Start the REST API:




CODE
go run rest-api/main.go
# REST Inventory API listening on :8080






Terminal 2 — Start the gRPC server:




CODE
go run server/main.go
# Inventory gRPC server listening at [::]:50051






Terminal 3 — Run the gRPC client:




CODE
go run client/main.go






Expected client output:




CODE
--- Executing Unary RPC ---
Stock check -> Amoxicillin 500mg: 1450 units (prescription required: true)

--- Executing Server-Streaming RPC ---
ALERT: Low stock -> Ibuprofen 200mg (SKU: MED-089) has only 12 units remaining.
ALERT: Low stock -> Lisinopril 10mg (SKU: MED-102) has only 5 units remaining.
ALERT: Low stock -> Cetirizine 10mg (SKU: MED-201) has only 8 units remaining.
--- Stream complete ---






Meanwhile, Terminal 2 (the gRPC server) will emit interceptor logs for each call:




CODE
[RPC] method=/inventory.InventoryService/CheckStock duration=2.1ms err=<nil>
[RPC] method=/inventory.InventoryService/StreamLowStock duration=1.8ms err=<nil>









Configuring the REST API URL



The gRPC server reads its upstream address from the REST_API_URL environment

variable, which defaults to http://localhost:8080 if unset. To point it at a

different host (e.g. in a Docker Compose network):




CODE
REST_API_URL=http://inventory-api:8080 go run server/main.go












Step 6: Debugging Binary Traffic with grpcurl



One of gRPC's stated drawbacks is that binary Protobuf payloads are opaque to

standard HTTP tools like curl. The idiomatic solution is

.






Engineering is best when shared. Find more of my writing, projects, and work at -- murtuza.dev

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
HP Announces an AMD-Powered Portable Workstation
1 Quelle
Microsoft’s design lead says users “can feel” Windows 11 speeding up, confirms major improvements this fall
1 Quelle
What I Actually Learned Adding Asgardeo Login to a Side Project
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Beyond REST: Architecting High-Performance Microservices with gRPC in Go

Thematisch verwandte Begriffe: Beyond, REST, Architecting, HighPerformance · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...