gRPC First Server and Client
This topic walks through building a complete, working gRPC server and client in Go. By the end you will have two programs that talk to each other using a proto-defined contract. The service calculates the area of geometric shapes — simple enough to focus on the gRPC mechanics, not the business logic.
Project Structure
shape-service/ ├── proto/ │ └── shape.proto ← contract definition ├── server/ │ └── main.go ← server implementation ├── client/ │ └── main.go ← client that calls the server ├── gen/ │ └── shape/ │ ├── shape.pb.go ← generated message code (auto) │ └── shape_grpc.pb.go ← generated service code (auto) └── go.mod
Step 1: Write the Proto File
// proto/shape.proto
syntax = "proto3";
package shape;
option go_package = "./gen/shape";
// The request carries the shape type and its dimensions
message ShapeRequest {
string shape_type = 1; // "circle", "rectangle", "triangle"
double dimension1 = 2; // radius for circle, width for rectangle/triangle
double dimension2 = 3; // unused for circle, height for rectangle/triangle
double dimension3 = 4; // unused for circle/rectangle, base for triangle
}
// The response carries the calculated area
message ShapeResponse {
double area = 1;
string shape_type = 2;
string formula = 3; // human-readable formula used
}
// The service exposes one unary method
service ShapeService {
rpc CalculateArea (ShapeRequest) returns (ShapeResponse);
}
Step 2: Generate Code from the Proto File
Install the compiler and plugins (one-time setup):
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Run the generator:
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
proto/shape.proto
What gets created:
gen/shape/shape.pb.go ← ShapeRequest and ShapeResponse structs
gen/shape/shape_grpc.pb.go ← ShapeServiceClient and ShapeServiceServer interfaces
Step 3: Implement the Server
// server/main.go
package main
import (
"context"
"fmt"
"log"
"math"
"net"
pb "shape-service/gen/shape"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// shapeServer embeds the generated unimplemented base
type shapeServer struct {
pb.UnimplementedShapeServiceServer
}
// CalculateArea is our actual business logic
func (s *shapeServer) CalculateArea(
ctx context.Context,
req *pb.ShapeRequest) (*pb.ShapeResponse, error) {
switch req.ShapeType {
case "circle":
r := req.Dimension1
area := math.Pi * r * r
return &pb.ShapeResponse{
Area: area,
ShapeType: "circle",
Formula: fmt.Sprintf("π × %.2f² = %.4f", r, area),
}, nil
case "rectangle":
w, h := req.Dimension1, req.Dimension2
area := w * h
return &pb.ShapeResponse{
Area: area,
ShapeType: "rectangle",
Formula: fmt.Sprintf("%.2f × %.2f = %.4f", w, h, area),
}, nil
case "triangle":
b, h := req.Dimension1, req.Dimension2
area := 0.5 * b * h
return &pb.ShapeResponse{
Area: area,
ShapeType: "triangle",
Formula: fmt.Sprintf("0.5 × %.2f × %.2f = %.4f", b, h, area),
}, nil
default:
// Return a proper gRPC error — not a panic or a log.Fatal
return nil, status.Errorf(codes.InvalidArgument,
"unknown shape type: %s", req.ShapeType)
}
}
func main() {
// 1. Listen on a TCP port
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 2. Create the gRPC server
grpcServer := grpc.NewServer()
// 3. Register our implementation with the gRPC server
pb.RegisterShapeServiceServer(grpcServer, &shapeServer{})
// 4. Start serving
log.Println("ShapeService listening on :50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Step 4: Write the Client
// client/main.go
package main
import (
"context"
"fmt"
"log"
"time"
pb "shape-service/gen/shape"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// 1. Open a channel to the server
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("connection failed: %v", err)
}
defer conn.Close()
// 2. Create the stub
client := pb.NewShapeServiceClient(conn)
// 3. Set a deadline — if the server takes longer than 5s, cancel the call
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 4. Call the server — feels like a local function call
shapes := []struct {
shapeType string
d1, d2, d3 float64
}{
{"circle", 5, 0, 0},
{"rectangle", 4, 6, 0},
{"triangle", 3, 8, 0},
{"hexagon", 2, 0, 0}, // will trigger an error
}
for _, s := range shapes {
resp, err := client.CalculateArea(ctx, &pb.ShapeRequest{
ShapeType: s.shapeType,
Dimension1: s.d1,
Dimension2: s.d2,
Dimension3: s.d3,
})
if err != nil {
fmt.Printf("Error for %s: %v\n", s.shapeType, err)
continue
}
fmt.Printf("Shape: %-12s Area: %8.4f Formula: %s\n",
resp.ShapeType, resp.Area, resp.Formula)
}
}
Step 5: Run and Test
Terminal 1 — start the server:
cd server
go run main.go
Output: ShapeService listening on :50051
Terminal 2 — run the client:
cd client
go run main.go
Expected output:
Shape: circle Area: 78.5398 Formula: π × 5.00² = 78.5398
Shape: rectangle Area: 24.0000 Formula: 4.00 × 6.00 = 24.0000
Shape: triangle Area: 12.0000 Formula: 0.5 × 3.00 × 8.00 = 12.0000
Error for hexagon: rpc error: code = InvalidArgument
desc = unknown shape type: hexagon
What Happens Under the Hood
When client.CalculateArea(ctx, req) runs:
Client gRPC Library Server
──────────────────────────────────────────────────────────────────
1. Serialise req to protobuf bytes
2. Open HTTP/2 stream on channel
3. Send HEADERS frame ─────────────────►
:path = /shape.ShapeService/CalculateArea
content-type = application/grpc
4. Send DATA frame (proto bytes) ─────────────────►
5. Deserialise bytes → ShapeRequest
6. Call CalculateArea method
7. Serialise response to bytes
◄─────────────────
8. Receive HEADERS frame (status)
9. Receive DATA frame (proto bytes)
10. Deserialise bytes → ShapeResponse
11. Return to application code
go.mod Dependencies
module shape-service go 1.22 require ( google.golang.org/grpc v1.64.0 google.golang.org/protobuf v1.34.1 )
Summary
Building a gRPC service in Go takes five steps: write the proto file, generate code with protoc, implement the server by satisfying the generated interface, write a client that creates a channel and a stub, and call the server methods. The business logic lives only in the server method implementation — everything else is generated or provided by the gRPC library.
