Skip to Content
Course content

66: Building a gRPC Service with Protocol Buffers

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

I've spent a lot of time building REST APIs, and while they're great for public-facing endpoints, they start to feel sluggish and "chatty" when you're connecting internal microservices. That's why I want to walk you through gRPC. Instead of sending bulky JSON strings back and forth, gRPC uses Protocol Buffers (protobufs) to send binary data. It's faster, smaller, and—my favorite part—it forces you to define a strict contract between the client and the server before you write a single line of logic.

Defining the Contract

Let's build a small "Stock Price Tracker." I want a service where a client can request the current price of a ticker symbol, and the server returns the price and the currency. In the gRPC world, we don't start with the Go code; we start with a .proto file. This is our source of truth.

syntax = "proto3";

package stocktracker;

// This is where I usually mess up the first time. 
// Go needs a specific option to know where the generated code goes.
option go_package = "./proto";

service PriceService {
  rpc GetPrice (PriceRequest) returns (PriceResponse) {}
}

message PriceRequest {
  string ticker = 1;
}

message PriceResponse {
  string ticker = 1;
  double price = 2;
  string currency = 3;
}

You'll notice those numbers like = 1 and = 2. Those aren't values; they're "field tags." Since we're sending binary data, gRPC doesn't send the word "ticker" over the wire. It just sends the tag number. If you change these numbers later, you'll break compatibility with older clients, so pick them and stick with them.

Wrestling with the Compiler

Now, I can't just run this file. I need to compile it into Go code. I'll run the protoc compiler. Initially, I might try something simple like protoc stock.proto, but that'll just fail because the compiler doesn't natively know how to write Go. I need the protoc-gen-go and protoc-gen-go-grpc plugins installed.

Here is the command I actually use to get the files generated in a dedicated folder:

protoc --go_out=. --go-grpc_out=. stock.proto

If you run this and see "command not found," it's almost always a $PATH issue with your Go bin directory. Once it works, you'll see a stock.pb.go and a stock_grpc.pb.go file. Don't touch those. Seriously. They are machine-generated, and if you edit them, you'll just lose your changes the next time you update the proto file.

Bringing the Interface to Life

Now for the fun part: making the server actually do something. When I look at the generated stock_grpc.pb.go, I see an interface called PriceServiceServer. My job is to create a struct that implements that interface.

type server struct {
	pb.UnimplementedPriceServiceServer
}

func (s *server) GetPrice(ctx context.Context, req *pb.PriceRequest) (*pb.PriceResponse, error) {
	fmt.Printf("Received request for: %s\n", req.GetTicker())
	
	// In a real app, this would be a DB call. For now, I'll hardcode it.
	if req.GetTicker() == "GOOG" {
		return &pb.PriceResponse{Ticker: "GOOG", Price: 2800.50, Currency: "USD"}, nil
	}
	
	return nil, status.Errorf(codes.NotFound, "Ticker %s not found", req.GetTicker())
}

Wait, I noticed something. If I just start the server now, it won't work because I haven't actually bound it to a port. I need to set up a TCP listener and register my server struct with the gRPC server instance. It's a bit more boilerplate than a standard http.ListenAndServe, but it's worth it for the performance gain.

lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
pb.RegisterPriceServiceServer(s, &server{})
s.Serve(lis)

Talking to the Server

Finally, let's try to hit this from a client. In REST, I'd use curl. But since gRPC uses a binary format (HTTP/2), curl isn't going to cut it. I have to use the generated client stub.

I'll try to connect using grpc.Dial. I'll use grpc.WithInsecure() for now because setting up TLS certificates for a local experiment is a headache I don't need right now.

conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()

client := pb.NewPriceServiceClient(conn)
resp, err := client.GetPrice(context.Background(), &pb.PriceRequest{Ticker: "GOOG"})
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Price: %f %s\n", resp.Price, resp.Currency)

When I run this, it's nearly instantaneous. The beauty here is that the client.GetPrice call feels like a local function call in my code, but under the hood, it's performing a highly optimized network request. If I decide to add a "Volume" field to my response later, I just update the .proto file, regenerate, and the compiler will tell me exactly which parts of my code need to be updated to handle the new field.




📋 Practical Task

Exercise: Implementing a Multi-Asset Crypto Ticker Service

Your goal is to expand the Stock Price Tracker into a Cryptocurrency Ticker service. You will need to modify the protobuf definition and implement the server logic to handle multiple different assets.

  • The Proto Change: Update the .proto file to include a new field in the PriceRequest called exchange (string). Update the PriceResponse to include a last_updated field (use google.protobuf.Timestamp).
  • The Server Logic: Implement a map in your server struct that stores prices for at least three different coins (e.g., BTC, ETH, SOL) across two different exchanges (e.g., "Binance", "Coinbase").
  • The Error Handling: If a user requests a coin that exists but specifies an exchange that doesn't carry that coin, return a codes.InvalidArgument gRPC error with a descriptive message.
  • The Client: Write a client that loops through a slice of three different tickers and prints their prices and the timestamp returned by the server.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.