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.

Leave a Comment

Your email address will not be published. Required fields are marked *