惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
S
SegmentFault 最新的问题
Project Zero
Project Zero
D
DataBreaches.Net
I
InfoQ
L
Lohrmann on Cybersecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
The Register - Security
The Register - Security
Recorded Future
Recorded Future
Vercel News
Vercel News
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
I
Intezer
The Hacker News
The Hacker News
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
Help Net Security
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Scott Helme
Scott Helme
T
Threatpost
爱范儿
爱范儿
N
Netflix TechBlog - Medium
D
Docker
云风的 BLOG
云风的 BLOG
C
Cisco Blogs
K
Kaspersky official blog
H
Help Net Security
S
Secure Thoughts
T
Threat Research - Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Security @ Cisco Blogs
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
G
Google Developers Blog
Forbes - Security
Forbes - Security
博客园 - 三生石上(FineUI控件)
博客园 - 叶小钗
B
Blog
Google DeepMind News
Google DeepMind News
Recent Announcements
Recent Announcements
Simon Willison's Weblog
Simon Willison's Weblog
S
Securelist
P
Privacy International News Feed
Spread Privacy
Spread Privacy
The Last Watchdog
The Last Watchdog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Beyond REST: Architecting High-Performance Microservices with gRPC in Go
Md Murtuza H · 2026-05-17 · via DEV Community

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:

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

Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode

Initialize your module:

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

Enter fullscreen mode Exit fullscreen mode

Project Structure

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

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

Enter fullscreen mode Exit fullscreen mode


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:

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;
}

Enter fullscreen mode Exit fullscreen mode

Generate the Go code from the project root:

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

Enter fullscreen mode Exit fullscreen mode

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:

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))
}

Enter fullscreen mode Exit fullscreen mode

Testing the REST API with curl

Start the REST API in one terminal:

go run rest-api/main.go

Enter fullscreen mode Exit fullscreen mode

Then verify each endpoint:

Look up a specific SKU:

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

[
  {
    "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
  }
]

Enter fullscreen mode Exit fullscreen mode

Confirm a 404 for an unknown SKU:

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

Enter fullscreen mode Exit fullscreen mode

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:

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)
    }
}

Enter fullscreen mode Exit fullscreen mode


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:

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 ---")
}

Enter fullscreen mode Exit fullscreen mode


Step 5: Running the Full Stack

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

Terminal 1 — Start the REST API:

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

Enter fullscreen mode Exit fullscreen mode

Terminal 2 — Start the gRPC server:

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

Enter fullscreen mode Exit fullscreen mode

Terminal 3 — Run the gRPC client:

go run client/main.go

Enter fullscreen mode Exit fullscreen mode

Expected client output:

--- 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 ---

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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):

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

Enter fullscreen mode Exit fullscreen mode


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
grpcurl — a command-line tool that
speaks gRPC natively, playing the same role curl plays for REST APIs.

Install it:

go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest

Enter fullscreen mode Exit fullscreen mode

To use grpcurl without a .proto file, enable server reflection by adding two
lines to server/main.go:

import "google.golang.org/grpc/reflection"

// Inside main(), after pb.RegisterInventoryServiceServer:
reflection.Register(s)

Enter fullscreen mode Exit fullscreen mode

Security note: Only enable reflection in development and staging environments.
Exposing your full service schema in production is an unnecessary attack surface.

With reflection running, you can introspect and invoke your service from the terminal,
in exactly the same workflow as curl + a REST API:

# List all available services
grpcurl -plaintext localhost:50051 list

# Describe the full InventoryService schema
grpcurl -plaintext localhost:50051 describe inventory.InventoryService

# Call CheckStock (unary)
grpcurl -plaintext \
  -d '{"sku": "MED-001"}' \
  localhost:50051 inventory.InventoryService/CheckStock

# Stream low-stock alerts (server-streaming)
grpcurl -plaintext \
  -d '{}' \
  localhost:50051 inventory.InventoryService/StreamLowStock

Enter fullscreen mode Exit fullscreen mode


Production Considerations

TLS

The examples above use insecure.NewCredentials(), transmitting data in plaintext.
For any non-local deployment, use mutual TLS (mTLS):

// Server-side
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
s := grpc.NewServer(grpc.Creds(creds), ...)

// Client-side
creds, err := credentials.NewClientTLSFromFile("ca.crt", "")
conn, err := grpc.NewClient("host:50051", grpc.WithTransportCredentials(creds))

Enter fullscreen mode Exit fullscreen mode

Chaining Multiple Interceptors

Real services need more than logging. Authentication, rate limiting, and panic recovery
are all common interceptor concerns. grpc.ChainUnaryInterceptor composes them
without nesting:

s := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        loggingInterceptor,
        authInterceptor,
        recoveryInterceptor,
    ),
    grpc.ChainStreamInterceptor(
        streamLoggingInterceptor,
    ),
)

Enter fullscreen mode Exit fullscreen mode

grpc.UnaryInterceptor only accepts a single function and panics if called more
than once — always prefer ChainUnaryInterceptor in production code.

HTTP Client Timeouts

The gRPC server's http.Client is configured with a 10-second timeout, but you
may want to tune this per-call using the incoming gRPC context directly, so that
a client's deadline propagates end-to-end:

// Pass the gRPC call context into the downstream HTTP request.
// If the gRPC client cancels, the REST API call is cancelled too.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)

Enter fullscreen mode Exit fullscreen mode

This pattern is already used in the server code above and prevents a situation where
a gRPC client times out but the REST API call continues to consume resources on the
server.


Architectural Realities: When (and When Not) to Use gRPC

While gRPC's performance characteristics are compelling, engineering is about
trade-offs.

Where gRPC Excels

Polyglot microservices. If your backend spans Go, Rust, Python, and Node.js,
Protobuf generates idiomatic client libraries for all of them from a single .proto
source of truth — with no per-language schema drift.

High-throughput internal APIs. Binary serialization is measurably faster to encode
and decode than JSON, and Protobuf payloads are significantly smaller over the wire —
particularly important at high request volumes.

Strict typing as a contract. Runtime errors caused by a misspelled JSON field
become a compile-time failure. The contract is enforced at build time, not discovered
in production.

Bidirectional streaming. gRPC supports client streaming, server streaming, and
full-duplex bidirectional streaming out of the box — capabilities that require
WebSockets or SSE to replicate in a REST architecture.

The Pragmatic Drawbacks

Layer 7 load balancing. Because gRPC multiplexes requests over long-lived HTTP/2
TCP connections, standard L4 load balancers (such as AWS NLB or a default Kubernetes
Service) will funnel all traffic from a single client pod to one server pod,
defeating horizontal scaling. You must front gRPC services with an L7 proxy — Envoy,
NGINX, or a full service mesh like Istio — to distribute load at the request level.

Browser incompatibility. Web browsers cannot natively initiate the HTTP/2
connections gRPC requires. Calling a gRPC service from a browser requires grpc-web
and an Envoy proxy as a translation layer. If your primary consumers are web or mobile
frontends, REST or GraphQL is architecturally simpler.

Operational tooling curve. Teams accustomed to curl, Postman, and browser
DevTools will need to adopt grpcurl or Postman's gRPC mode. Binary traffic is
invisible without the right tools in place — which is precisely why the grpcurl
section above is not optional reading.


Summary

This guide built a realistic three-tier system: a REST data layer verified directly
with curl, a gRPC server that calls it, and a typed gRPC client that consumes the
result. Each tier can be developed, tested, and debugged in isolation — the REST API
with curl, and the gRPC surface with grpcurl.

gRPC in Go provides a robust foundation for enterprise-grade internal services, but it
should be adopted rationally. Use it where strict contracts, low latency, and efficient
serialization are genuine architectural requirements — not merely because it is the
current industry trend.

The full source code for this tutorial is available at
github.com/mmurtuza/grpc-inventory.


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