Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
66: Building a gRPC Service with Protocol Buffers
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
.protofile to include a new field in thePriceRequestcalledexchange(string). Update thePriceResponseto include alast_updatedfield (usegoogle.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.InvalidArgumentgRPC 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.
There are no comments for now.