Skip to Content
Course content

56: gRPC Services in Go

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you work in a massive corporate headquarters. You need a specific report from the Finance department on the 10th floor. You can't just wander into their office and start chatting; they have a strict protocol. To get what you need, you must fill out a very specific "Request Form" (Form 10-A). This form has exact fields: a Request ID, the Date Range, and the Department Code. If you leave a field blank or write "yesterday" instead of a formatted date, the clerk will simply hand the form back to you without processing it.

Once you submit that form via an internal courier, the courier doesn't just carry a piece of paper; they use a high-speed pneumatic tube system to zip it across the building. The Finance clerk receives the form, does the work, fills out a "Response Form" (Form 10-B), and zips it back to you. You didn't need to know how the Finance department is organized or what software they use—you just needed the form and the courier.

That's exactly how gRPC works. The "Request Form" is your Protocol Buffer (.proto) file. The "Pneumatic Tube" is HTTP/2. And the act of the clerk processing the request is the gRPC Service implementation in Go. Unlike a REST API where you send JSON (which is like sending a handwritten letter that might be misinterpreted), gRPC is strictly typed. If the proto file says a field is an integer, you cannot send a string. Period.

Defining the Handshake with Protocol Buffers

Before we write a single line of Go, we have to define the contract. I've found that most developers rush this part, but in gRPC, the .proto file is the single source of truth. Let's imagine we are building a Payment Processing Service.

syntax = "proto3";

package payment;

// The Go package path where the generated code will live
option go_package = "./pb";

service PaymentService {
  // A simple Unary RPC: one request, one response
  rpc ProcessPayment (PaymentRequest) returns (PaymentResponse) {}
}

message PaymentRequest {
  string order_id = 1;
  double amount = 2;
  string currency = 3;
}

message PaymentResponse {
  bool success = 1;
  string transaction_id = 2;
}

Notice those numbers (= 1, = 2) next to the fields? Those aren't values; they are "tags." Because gRPC sends data in a binary format rather than text, it uses these tags to identify fields. This is why gRPC is so much faster and smaller than JSON—it doesn't have to send the words "order_id" over the wire every single time.

Turning the Interface into Action

Once you run the protoc compiler, Go generates an interface. Your job is to create a struct that satisfies that interface. I like to think of this as filling in the blanks of a template. You don't worry about the networking or the serialization; you just write the business logic.

type paymentServer struct {
    pb.UnimplementedPaymentServiceServer
}

func (s *paymentServer) ProcessPayment(ctx context.Context, req *pb.PaymentRequest) (*pb.PaymentResponse, error) {
    log.Printf("Processing payment for Order: %s", req.OrderId)
    
    // In a real app, you'd hit a database or a payment gateway here
    if req.Amount <= 0 {
        return nil, status.Errorf(codes.InvalidArgument, "Amount must be greater than zero")
    }

    return &pb.PaymentResponse{
        Success:       true,
        TransactionId: "txn_abc123",
    }, nil
}

A quick tip: notice the status.Errorf. In gRPC, you don't just return a generic error string. You use specific gRPC codes (like InvalidArgument or NotFound). This allows the client to know exactly why the request failed without having to parse a text message.

Connecting the Client to the Wire

On the client side, you don't manually construct HTTP requests. You create a "stub"—a local object that looks and acts like the remote service. When you call a method on that stub, the gRPC library handles the magic of turning your Go struct into binary, sending it over HTTP/2, and waiting for the response.

conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
    log.Fatalf("did not connect: %v", err)
}
defer conn.Close()

client := pb.NewPaymentServiceClient(conn)

resp, err := client.ProcessPayment(context.Background(), &pb.PaymentRequest{
    OrderId:  "order_99",
    Amount:   49.99,
    Currency: "USD",
})

One thing to keep in mind: because gRPC uses HTTP/2, it keeps the connection open. You aren't opening and closing a socket for every single single call. This is a huge performance win when you have hundreds of microservices talking to each other constantly.




📋 Practical Task

Build a Distributed Inventory Stock Checker

Your task is to implement a small gRPC system that checks if an item is in stock across different warehouses.

Requirements:

  • The Contract: Create a .proto file defining an InventoryService. It should have one RPC method called CheckStock. The request should take an item_id (string) and the response should return in_stock (boolean) and quantity (int32).
  • The Server: Implement the server in Go. Instead of a database, use a simple map[string]int to store a few hardcoded items (e.g., "laptop": 5, "mouse": 0). If an item_id isn't in the map, return a gRPC NotFound error.
  • The Client: Write a client that requests the stock for one existing item and one non-existent item, printing the results (or the error) to the console.

Goal: Successfully compile the proto file, start the server on port :50052, and verify that the client receives both a valid quantity and a NotFound error.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.