This episode turns a .proto file into real code: installing plugins, the protoc command for Go, implementing a simple server, implementing a simple client, then local testing with a request-response round trip that runs successfully for the first time.

A contract without code is just a document. Episode 4 turns your .proto into an application that actually runs: generating code with protoc, implementing the server, creating the client, and seeing your first request-response succeed. This is the most exciting moment in learning gRPC — theory starts becoming real.
We use Go because its syntax is concise and its toolchain is very solid for gRPC. The same principles apply to Node.js, Python, or Java: the contract is the same, the protoc command is similar, and only the language plugin and generated syntax differ.
Make sure the Go plugins are installed as prepared in episode 0, then create the project structure and pull the gRPC dependencies:
mkdir -p proto gen/catalog/v1
go get google.golang.org/grpc
go get google.golang.org/protobufThe proto/ directory holds your .proto files, and gen/ becomes the output location for generated code. The go get google.golang.org/grpc command pulls the gRPC runtime and its dependencies into go.mod.
With the plugins installed, generate code from the product.proto contract in episode 3:
protoc -I proto \
--go_out=gen --go_opt=paths=source_relative \
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
proto/catalog/v1/product.protoThe protoc -I proto command declares the import search root. After it runs, two files are born in gen/catalog/v1/: product.pb.go contains the message definitions, and product_grpc.pb.go contains the client stub and server interface.
Now implement the generated interface. Create server/main.go:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "learn-grpc/gen/catalog/v1"
)
type catalogServer struct {
pb.UnimplementedCatalogServiceServer
}
func (s *catalogServer) GetProduct(
ctx context.Context, in *pb.ProductId,
) (*pb.Product, error) {
return &pb.Product{
Id: in.Id,
Name: "Kursi Kantor",
Price: 450000,
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
pb.RegisterCatalogServiceServer(s, &catalogServer{})
log.Println("server listening di :50051")
if err := s.Serve(lis); err != nil {
log.Fatal(err)
}
}There are three important things here: net.Listen("tcp", ":50051") opens port 50051, pb.RegisterCatalogServiceServer registers the implementation, and s.Serve(lis) starts serving requests. Embedding UnimplementedCatalogServiceServer keeps the server compiling even if some methods aren't implemented yet.
On the client side, create client/main.go using the generated stub:
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "learn-grpc/gen/catalog/v1"
)
func main() {
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := pb.NewCatalogServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
res, err := client.GetProduct(ctx, &pb.ProductId{Id: "p-001"})
if err != nil {
log.Fatal(err)
}
log.Printf("produk: %s dengan harga %.0f", res.Name, res.Price)
}pb.NewCatalogServiceClient(conn) creates a stub that's ready to use, and context.WithTimeout gives the call a five-second limit — the deadline pattern from episode 2.
Run the server in one terminal and the client in another:
go run ./serverOnce the server log appears, from another terminal:
go run ./clientThe client output should show produk: Kursi Kantor dengan harga 450000. If you get a connection refused error, check that the server is actually running and port 50051 isn't blocked. The go run ./client path is the end of the generate-to-execute pipeline — from .proto to a running application.
Key takeaways:
protoc -I proto ... --go_out generates messages; --go-grpc_out generates the service.RegisterCatalogServiceServer and runs on port 50051.In episode 5 next, we cover unary, streaming, and basic error handling — implementing the four RPC patterns declared in episode 3, mapping status codes to business scenarios, rich error details, and handling deadlines and cancellation on both the client and server side. Your first server-client will transform into a complete service.