gRPC and Protocol Buffers
Reviewed & published by Brayan K
gRPC lets Go services call each other like local functions over the network. You define a contract once in a .proto file, generate typed client and server code with protoc, and get fast binary transport plus streaming over HTTP/2 — a strongly typed alternative to REST/JSON.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Define messages and services in a .proto file
- Generate Go code with protoc and protoc-gen-go
- Build a server with grpc.NewServer() and register a service
- Dial a server from a client and call a method
- Tell unary, server, client, and bidirectional streaming apart
- See why gRPC differs from REST/JSON
1️⃣ The contract: a .proto file
Everything starts with the schema. A message is a typed struct; each field has a type, a name, and a unique field number (its tag on the wire). A service groups RPC methods. From this one file, protoc generates both the Go structs and the client/server stubs.
// greeter.proto — the CONTRACT shared by client and server.
// syntax tells protoc which Protocol Buffers version to use.
syntax = "proto3";
// The Go import path for the generated code.
option go_package = "example.com/greeter/pb";
package greeter;
// A "message" is a typed struct. Each field has a TYPE, a NAME, and a
// unique FIELD NUMBER (the tag on the wire). Numbers must stay stable
// across versions so old and new code remain compatible.
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
// A "service" is a set of RPC methods. This one has a single UNARY
// method: one request in, one response out.
service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply);
}2️⃣ The server: grpc.NewServer() and register
Implement the generated GreeterServer interface, then wire it up in three steps: create the server with grpc.NewServer(), register your implementation with pb.RegisterGreeterServer, and call Serve on a listener. Embedding UnimplementedGreeterServer keeps you forward-compatible.
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "example.com/greeter/pb" // the generated package
)
// Implement the GreeterServer interface that protoc generated.
// Embedding UnimplementedGreeterServer keeps you forward-compatible
// if new methods are added to the .proto later.
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello is the UNARY handler: one request, one response.
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello, " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051") // gRPC's conventional port
if err != nil {
log.Fatalf("listen failed: %v", err)
}
grpcServer := grpc.NewServer() // 1) create the server
pb.RegisterGreeterServer(grpcServer, &server{}) // 2) register impl
log.Println("gRPC server on :50051")
if err := grpcServer.Serve(lis); err != nil { // 3) block and serve
log.Fatalf("serve failed: %v", err)
}
}3️⃣ The client: grpc.Dial and call
The client dials the server to get a connection, wraps it in the generated client, and then calls methods as if they were local. Pass a context with a deadline so a hung server can't block you forever.
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "example.com/greeter/pb"
)
func main() {
// Dial the server to get a connection. insecure.NewCredentials()
// is for local/dev only — use TLS credentials in production.
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("dial failed: %v", err)
}
defer conn.Close()
// The generated client wraps the connection in typed methods.
client := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Calling SayHello looks like a local function call — but it's an RPC.
reply, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Ada"})
if err != nil {
log.Fatalf("rpc failed: %v", err)
}
log.Println("server said:", reply.GetMessage())
}4️⃣ Streaming: more than one message
Unary is one-in, one-out — but gRPC also streams. Mark a method with the stream keyword in the .proto and the handler gets a stream to Send on (or Recv from) instead of a single return value.
// In the .proto, the "stream" keyword marks streaming methods:
//
// service Greeter {
// // SERVER streaming: one request, many replies.
// rpc SayHellos(HelloRequest) returns (stream HelloReply);
// }
// The generated server handler receives a STREAM to send on, not a
// return value. Call stream.Send(...) as many times as you like.
func (s *server) SayHellos(in *pb.HelloRequest, stream pb.Greeter_SayHellosServer) error {
for i := 0; i < 3; i++ {
if err := stream.Send(&pb.HelloReply{Message: "Hi " + in.GetName()}); err != nil {
return err
}
}
return nil // returning ends the stream
}
// The four RPC kinds gRPC supports:
// Unary : 1 request -> 1 response
// Server stream : 1 request -> many responses
// Client stream : many reqs -> 1 response
// Bidirectional : many reqs <-> many responses (over one HTTP/2 conn)🎯 Your Turn
Finish this unary handler. Fill in the two blanks marked ___ so the server greets the caller by name.
package main
import (
"context"
pb "example.com/greeter/pb"
)
type server struct {
pb.UnimplementedGreeterServer
}
// 🎯 YOUR TURN — fill in the blanks marked ___
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
// 1) Read the caller's name from the request
name := in.___() // 👉 GetName()
// 2) Return a reply message and a nil error
return &pb.HelloReply{Message: "Hello, " + name}, ___ // 👉 nil
}
// ✅ Expected: a client calling SayHello with Name:"Ada"
// receives Message: "Hello, Ada"Common Errors (and the fix)
❌ Editing the generated .pb.go files — your changes vanish on regeneration.
✅ Change the .proto and re-run protoc; never hand-edit generated code.
❌ Renumbering or reusing a field number — breaks already-deployed clients.
✅ Keep field numbers stable forever; add new fields with new numbers.
❌ Forgetting transport credentials on the client — the dial fails.
✅ For local dev pass insecure.NewCredentials(); use TLS in production.
❌ Not embedding UnimplementedGreeterServer — code breaks when new methods are added.
✅ Embed it so unimplemented methods return a clear error instead of failing to compile.
🧠 Quick Recall
one request, many responses = which RPC kind?
Server streaming. Mark it in the .proto with returns (stream Reply); the handler calls stream.Send repeatedly.
what does the field number after a field mean?
It is the wire tag identifying that field in the binary encoding. It must stay constant across versions for compatibility.
🎯 Mini-Challenge: A Calculator Service
Extend the contract: add AddRequest, AddReply, and an Add RPC on a Calculator service, then regenerate.
// greeter.proto
syntax = "proto3";
option go_package = "example.com/greeter/pb";
package greeter;
// 🎯 MINI-CHALLENGE: extend the contract.
// 1. Add a message "AddRequest" with two int32 fields: a = 1, b = 2.
// 2. Add a message "AddReply" with one int32 field: sum = 1.
// 3. Add a UNARY rpc "Add(AddRequest) returns (AddReply)" to a
// service named "Calculator".
//
// ✅ After running protoc, a CalculatorServer interface with an
// Add(ctx, *AddRequest) (*AddReply, error) method is generated.
// your messages and service here🎉 Lesson Complete!
- ✅ A .proto file defines messages and services — the single contract
- ✅ protoc + protoc-gen-go generate typed Go stubs
- ✅ Build a server with grpc.NewServer(), register, and Serve
- ✅ Clients Dial the server and call methods like local functions
- ✅ RPCs come in unary, server, client, and bidirectional streaming flavours
Practice quiz
What is a .proto file used for in gRPC?
- Defining messages and services as the contract between client and server
- Configuring the network firewall
- Logging RPC traffic
- Storing compiled binaries
Answer: Defining messages and services as the contract between client and server. The .proto file is the source of truth: it declares your messages and the service methods both sides agree on.
Which tool generates Go code from a .proto file?
- grpcurl
- go build
- protoc together with protoc-gen-go
- gofmt
Answer: protoc together with protoc-gen-go. protoc is the Protocol Buffer compiler; the protoc-gen-go and protoc-gen-go-grpc plugins emit the Go types and stubs.
How does Protocol Buffers send data on the wire?
- As CSV rows
- As a compact binary format
- As human-readable JSON
- As XML
Answer: As a compact binary format. Protobuf serializes to a compact, efficient binary representation, which is smaller and faster to parse than JSON text.
What does the field number after each field in a message mean (e.g. name = 1)?
- A default value
- The maximum length
- An array index that must start at zero
- A unique tag identifying that field on the wire
Answer: A unique tag identifying that field on the wire. Field numbers are the stable wire identifiers; they must stay constant across versions so old and new code stay compatible.
Which function creates a new gRPC server in Go?
- grpc.NewServer()
- grpc.Open()
- grpc.Listen()
- grpc.Serve()
Answer: grpc.NewServer(). grpc.NewServer() returns a *grpc.Server; you then register your service and call Serve on a net.Listener.
How does a client establish a connection to a gRPC server?
- grpc.Accept()
- grpc.Dial (or grpc.NewClient) to get a *grpc.ClientConn
- http.Get on the service URL
- net.Listen on the port
Answer: grpc.Dial (or grpc.NewClient) to get a *grpc.ClientConn. The client dials the target address to obtain a *grpc.ClientConn, which it passes to the generated client constructor.
What is a UNARY RPC?
- A call that never returns
- A call with no request message
- A stream of requests
- A single request that returns a single response
Answer: A single request that returns a single response. Unary is the simplest pattern: one request in, one response out, like a normal function call over the network.
Which streaming pattern lets BOTH sides send a stream of messages at once?
- Client streaming
- Unary
- Bidirectional streaming
- Server streaming
Answer: Bidirectional streaming. Bidirectional streaming opens two independent streams so client and server can read and write concurrently.
After registering your implementation, which call starts handling requests?
- server.Run()
- server.Serve(listener)
- server.Start()
- server.Handle()
Answer: server.Serve(listener). grpcServer.Serve(lis) blocks and serves incoming connections on the provided net.Listener until it stops.
A key advantage of gRPC over plain REST/JSON is that it...
- generates strongly typed client and server code from one contract
- requires no schema at all
- only works in the browser
- cannot do streaming
Answer: generates strongly typed client and server code from one contract. From a single .proto contract gRPC generates typed stubs for both sides, plus efficient binary transport and built-in streaming over HTTP/2.
Continue this course
- Previous: Testing in Go
- Next: Database Access with database/sql — Query and modify SQL databases with the stdlib pool, scanning and transactions
Frequently asked questions
What exactly is a .proto file and why is it the contract?
A .proto file is a language-neutral schema written in Protocol Buffers syntax. It declares your messages (typed structs) and services (sets of RPC methods). Both the client and server are generated from this one file, so it is the single source of truth for the API. Change the .proto, regenerate, and both sides stay in sync — there is no separate hand-written request/response code to drift apart.
How do I turn a .proto into Go code?
Run the Protocol Buffer compiler protoc with the Go plugins: protoc --go_out=. --go-grpc_out=. greeter.proto. The protoc-gen-go plugin emits the message structs (greeter.pb.go) and protoc-gen-go-grpc emits the server interface and client stub (greeter_grpc.pb.go). You implement the generated server interface and call the generated client methods.
Why use Protocol Buffers instead of JSON?
Protobuf serializes to a compact binary format that is smaller on the wire and much faster to encode and decode than JSON text. It is also strongly typed and schema-first: the field numbers give you forward and backward compatibility, and code is generated for you so there is no manual parsing. gRPC runs this over HTTP/2, which adds multiplexing and built-in streaming.
What are the four kinds of RPC?
Unary is one request and one response, like a normal function call. Server streaming sends one request and receives many responses. Client streaming sends many requests and receives one response. Bidirectional streaming opens two independent streams so both sides send and receive at once over a single HTTP/2 connection. You pick the pattern in the .proto with the stream keyword.
How do the server and client connect?
On the server you call grpc.NewServer(), register your implementation with the generated RegisterXxxServer function, then call Serve on a net.Listener. On the client you dial the address (grpc.NewClient or grpc.Dial) to get a *grpc.ClientConn, then pass it to the generated NewXxxClient constructor. After that, calling a method looks just like a local function call, but it travels over the network.
When should I choose gRPC over REST/JSON?
Prefer gRPC for internal service-to-service communication where you want strong typing, low latency, efficient binary payloads, and streaming — for example a microservices backend. REST with JSON is often better for public, browser-facing APIs because it is human-readable, cache-friendly, and works everywhere without code generation. Many systems use both: gRPC inside, REST at the edge.