# gRPC services

> Define a service in Protocol Buffers, generate Go code, implement server and client with deadlines, status codes, interceptors and streaming.

Backend with Go roadmap · Building APIs · 18 min read · https://levelcode.org/roadmaps/go-backend/grpc

REST with JSON is the right default for public APIs: every language and tool speaks it. Between your own services, many Go teams use **gRPC** instead. You describe the API once in a `.proto` file, generate typed client and server code for every language, and get compact binary messages over HTTP/2 with deadlines and streaming built in. This lesson builds an orders service with gRPC in Go, and ends with when REST is still the better choice.

## What gRPC gives you

gRPC is a remote procedure call framework: the client calls a method on a generated stub as if it were local, and the framework handles encoding, transport and errors.

| | REST + JSON | gRPC |
|---|---|---|
| Contract | OpenAPI, often written after the code | `.proto` file is the source of truth; code is generated from it |
| Encoding | JSON text | Protocol Buffers binary: smaller and faster to parse |
| Transport | HTTP/1.1 or HTTP/2 | HTTP/2 only (multiplexed, long-lived connections) |
| Streaming | Server-sent events or WebSockets, bolted on | Client, server and bidirectional streams built in |
| Browser support | Native | Needs a proxy or gRPC-Web/Connect |
| Debugging | `curl` and readable payloads | Needs tools such as `grpcurl` |

The headline benefit isn't speed, though it is faster. It's that the **contract is enforced by the compiler** across every service and language that uses it.

## Defining the service

Create `proto/orders/v1/orders.proto`:

```proto
syntax = "proto3";

package orders.v1;

option go_package = "example.com/shop/gen/orders/v1;ordersv1";

import "google/protobuf/timestamp.proto";

service OrderService {
  rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
  rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);
}

message Order {
  string id = 1;
  string customer_id = 2;
  repeated LineItem items = 3;
  int64 total_cents = 4;
  Status status = 5;
  google.protobuf.Timestamp created_at = 6;
}

message LineItem {
  string sku = 1;
  int32 quantity = 2;
}

enum Status {
  STATUS_UNSPECIFIED = 0;
  STATUS_PENDING = 1;
  STATUS_PAID = 2;
  STATUS_SHIPPED = 3;
}

message GetOrderRequest { string id = 1; }
message GetOrderResponse { Order order = 1; }

message CreateOrderRequest {
  string customer_id = 1;
  repeated LineItem items = 2;
}
message CreateOrderResponse { Order order = 1; }

message WatchOrderRequest { string id = 1; }
message OrderEvent {
  Status status = 1;
  google.protobuf.Timestamp at = 2;
}
```

The numbers are **field tags**, and they're what goes on the wire, not the names. That gives you the rules for evolving a schema without breaking deployed clients:

- Never change or reuse a field's number. Removed a field? Add `reserved 7;` so nobody reuses it.
- Adding fields is safe: old readers ignore unknown fields, and new readers see the zero value when an old writer omits it.
- In proto3, every field has a zero value, so "not set" and "set to 0" look the same. Enums start with an `_UNSPECIFIED = 0` value for that reason. Mark a scalar `optional` when you need to tell them apart.
- Put a version in the package (`orders.v1`). A breaking change becomes `orders.v2`, served alongside v1 while clients migrate.
- Give every RPC its own request and response message, even if one has a single field. You can add fields later without changing the method signature.

## Generating Go code

Install the compiler plugins and generate:

```bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

mkdir -p gen
protoc -I proto \
  --go_out=gen --go_opt=paths=source_relative \
  --go-grpc_out=gen --go-grpc_opt=paths=source_relative \
  proto/orders/v1/orders.proto
```

This needs `protoc` itself (from your package manager). Many teams use `buf` instead, which adds linting and a check that fails your build on breaking schema changes. Either way you get two files: `orders.pb.go` with the message types and `orders_grpc.pb.go` with a client and a server interface. Commit the generated code, or generate it in CI; just be consistent.

## Implementing the server

Implement the generated `OrderServiceServer` interface. Embedding `UnimplementedOrderServiceServer` makes methods you haven't written yet return `Unimplemented`, so adding an RPC to the proto doesn't break compilation of older servers:

```go
package main

import (
	"context"
	"errors"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"

	ordersv1 "example.com/shop/gen/orders/v1"
)

type orderServer struct {
	ordersv1.UnimplementedOrderServiceServer
	store *Store
}

func (s *orderServer) GetOrder(ctx context.Context, req *ordersv1.GetOrderRequest) (*ordersv1.GetOrderResponse, error) {
	if req.GetId() == "" {
		return nil, status.Error(codes.InvalidArgument, "id is required")
	}
	o, err := s.store.Get(ctx, req.GetId())
	if errors.Is(err, ErrNotFound) {
		return nil, status.Errorf(codes.NotFound, "order %s not found", req.GetId())
	}
	if err != nil {
		return nil, status.Error(codes.Internal, "could not load order")
	}
	return &ordersv1.GetOrderResponse{Order: toProto(o)}, nil
}

// toProto converts the domain type to the wire type in one place.
func toProto(o Order) *ordersv1.Order {
	items := make([]*ordersv1.LineItem, len(o.Items))
	for i, it := range o.Items {
		items[i] = &ordersv1.LineItem{Sku: it.SKU, Quantity: it.Quantity}
	}
	return &ordersv1.Order{
		Id:         o.ID,
		CustomerId: o.CustomerID,
		Items:      items,
		TotalCents: o.TotalCents,
		Status:     o.Status,
		CreatedAt:  timestamppb.New(o.CreatedAt),
	}
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatal(err)
	}
	srv := grpc.NewServer(grpc.ChainUnaryInterceptor(logUnary)) // logUnary is defined below
	ordersv1.RegisterOrderServiceServer(srv, &orderServer{store: NewStore()})
	log.Fatal(srv.Serve(lis))
}
```

`Order` and `Store` are your own domain types; `toProto` keeps the generated types at the edge of the service instead of leaking them into business logic. Use the generated `GetX()` getters rather than fields: they're nil-safe, so `req.GetOrder().GetId()` returns `""` instead of panicking when `Order` is missing.

### Status codes

gRPC errors carry a code, much like HTTP status codes. Return them deliberately; clients and retry policies depend on them:

| Code | Use when | Rough HTTP equivalent |
|---|---|---|
| `InvalidArgument` | The request is malformed | 400 |
| `NotFound` | The resource doesn't exist | 404 |
| `AlreadyExists` | A create conflicts | 409 |
| `PermissionDenied` / `Unauthenticated` | Not allowed / no valid credentials | 403 / 401 |
| `FailedPrecondition` | Valid request, wrong state (shipping an unpaid order) | 400 |
| `Aborted` | A concurrency conflict, such as a failed optimistic lock; retry the whole operation | 409 |
| `Unavailable` | Temporarily down; safe to retry | 503 |
| `DeadlineExceeded` | Ran out of time | 504 |
| `Internal` | A bug or unexpected failure | 500 |

The HTTP column follows Google's standard mapping, which tools such as grpc-gateway use. Never return a raw Go error: it becomes `Unknown`, and its message (database hostnames, SQL errors) is sent to the client verbatim. Map domain errors to codes in one place, as above.

## The client and deadlines

```go
conn, err := grpc.NewClient("localhost:50051",
	grpc.WithTransportCredentials(insecure.NewCredentials())) // TLS in production
if err != nil {
	log.Fatal(err)
}
defer conn.Close()

client := ordersv1.NewOrderServiceClient(conn)

ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()

resp, err := client.GetOrder(ctx, &ordersv1.GetOrderRequest{Id: "o_123"})
if err != nil {
	switch status.Code(err) {
	case codes.NotFound:
		fmt.Println("no such order")
	case codes.DeadlineExceeded:
		fmt.Println("orders service too slow")
	default:
		fmt.Println("error:", err)
	}
	return
}
fmt.Println(resp.GetOrder().GetTotalCents())
```

`grpc.NewClient` doesn't connect immediately; it connects on first use and reconnects automatically. Create one `*grpc.ClientConn` per target at startup and share it: it multiplexes all calls over HTTP/2 connections.

**Always set a deadline.** gRPC sends the deadline to the server in a header, and the server's `ctx` is cancelled when it passes. If the orders service then calls the inventory service with the same `ctx`, the remaining time propagates automatically. A request with no deadline can hang forever when a downstream service stalls.

## Interceptors

Interceptors are gRPC's middleware. A unary server interceptor wraps every non-streaming call, which makes it the place for logging, metrics, authentication and panic recovery:

```go
func logUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
	start := time.Now()
	resp, err := handler(ctx, req)
	slog.InfoContext(ctx, "rpc",
		"method", info.FullMethod,
		"code", status.Code(err).String(),
		"duration", time.Since(start))
	return resp, err
}
```

Metadata (gRPC's headers) carries things like bearer tokens: read it with `metadata.FromIncomingContext(ctx)` in an auth interceptor, and return `codes.Unauthenticated` when it's missing. For tracing and metrics, use the OpenTelemetry gRPC instrumentation rather than writing your own.

## Streaming

`WatchOrder` returns `stream OrderEvent`: the server sends any number of messages on one call. The generated handler gets a stream to `Send` on:

```go
func (s *orderServer) WatchOrder(req *ordersv1.WatchOrderRequest, stream grpc.ServerStreamingServer[ordersv1.OrderEvent]) error {
	updates := s.store.Subscribe(req.GetId())
	defer s.store.Unsubscribe(req.GetId(), updates)
	for {
		select {
		case <-stream.Context().Done():
			return stream.Context().Err() // gRPC reports this as Canceled or DeadlineExceeded
		case st, ok := <-updates:
			if !ok {
				return nil // the store closed the subscription: end the stream cleanly
			}
			if err := stream.Send(&ordersv1.OrderEvent{Status: st, At: timestamppb.Now()}); err != nil {
				return err
			}
		}
	}
}
```

The client calls `stream, err := client.WatchOrder(ctx, req)` and loops on `stream.Recv()` until it returns `io.EOF`. Client streaming (`rpc Upload(stream Chunk) returns (Summary)`) and bidirectional streaming follow the same pattern in the other direction. Long-lived streams should still have a deadline or a way to be cancelled.

## When to choose REST, gRPC or both

- **Public APIs and browser clients**: REST with JSON. Everybody can call it, and caches, proxies and browsers understand it.
- **Service-to-service inside your system**, especially across languages or with streaming: gRPC.
- **Both from one definition**: `grpc-gateway` generates a REST/JSON proxy from annotations in the proto, and the Connect protocol (`connectrpc.com/connect`) serves gRPC, gRPC-Web and plain JSON-over-HTTP from the same Go handlers, which browsers can call directly.

Don't adopt gRPC for a single monolith with a React frontend; the generation step and tooling cost buys you nothing there.

## Practice

1. Write the proto above, generate the code, and implement `GetOrder` and `CreateOrder` with an in-memory store. `CreateOrder` must return `InvalidArgument` when `items` is empty or any quantity is below 1.
2. Add an auth interceptor that requires metadata `authorization: Bearer dev-token` and returns `Unauthenticated` otherwise.
3. Write a client that creates an order and fetches it, with a 1-second deadline.
4. Add `reserved` handling: remove `customer_id` from `Order`, reserve its number and name, and regenerate.

**Solution**

```go
func (s *orderServer) CreateOrder(ctx context.Context, req *ordersv1.CreateOrderRequest) (*ordersv1.CreateOrderResponse, error) {
	if len(req.GetItems()) == 0 {
		return nil, status.Error(codes.InvalidArgument, "items must not be empty")
	}
	for i, it := range req.GetItems() {
		if it.GetQuantity() < 1 {
			return nil, status.Errorf(codes.InvalidArgument, "items[%d].quantity must be at least 1", i)
		}
	}
	items := make([]LineItem, len(req.GetItems()))
	for i, it := range req.GetItems() {
		items[i] = LineItem{SKU: it.GetSku(), Quantity: it.GetQuantity()}
	}
	o := s.store.Create(ctx, req.GetCustomerId(), items)
	return &ordersv1.CreateOrderResponse{Order: toProto(o)}, nil
}

func authUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
	md, _ := metadata.FromIncomingContext(ctx)
	if v := md.Get("authorization"); len(v) == 0 || v[0] != "Bearer dev-token" {
		return nil, status.Error(codes.Unauthenticated, "missing or invalid token")
	}
	return handler(ctx, req)
}

// server: grpc.NewServer(grpc.ChainUnaryInterceptor(logUnary, authUnary))
```

```go
// client
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer dev-token")

created, err := client.CreateOrder(ctx, &ordersv1.CreateOrderRequest{
	CustomerId: "c_1",
	Items:      []*ordersv1.LineItem{{Sku: "KB-01", Quantity: 1}},
})
if err != nil {
	log.Fatal(err)
}
got, err := client.GetOrder(ctx, &ordersv1.GetOrderRequest{Id: created.GetOrder().GetId()})
if err != nil {
	log.Fatal(err)
}
fmt.Println(got.GetOrder().GetId(), got.GetOrder().GetStatus()) // o_1 STATUS_PENDING
```

```proto
message Order {
  reserved 2;
  reserved "customer_id";
  string id = 1;
  repeated LineItem items = 3;
  int64 total_cents = 4;
  Status status = 5;
  google.protobuf.Timestamp created_at = 6;
}
```

After regenerating, the Go code that set `CustomerId` on `Order` stops compiling, which is the point: the compiler finds every place that relied on the removed field. Reserving the number stops anyone from reusing `2` for a different field, which would make old clients misread new messages.

## Key takeaways

- gRPC generates typed clients and servers from a `.proto` contract and sends compact Protocol Buffers over HTTP/2.
- Field numbers are the wire format: never reuse them, reserve removed ones, and version packages for breaking changes.
- Return explicit status codes such as `NotFound` and `InvalidArgument`; never leak raw errors as `Unknown`.
- Share one `ClientConn` per target, and set a deadline on every call so timeouts propagate through the call chain.
- Use interceptors for cross-cutting concerns, and prefer REST (or Connect/grpc-gateway alongside gRPC) for browsers and public APIs.

## Quiz

1. You delete a field from a proto message. What should you do with its number?

   A. Reuse it for the next new field to keep numbers compact
   B. Renumber all later fields
   C. Mark it reserved so it can never be reused
   D. Nothing; numbers don't matter on the wire

   **Answer: C.** The wire format identifies fields by number. Reusing a number makes old clients decode a new field as the old one. Reserving it prevents that.

2. A gRPC handler returns errors.New("db timeout") directly. What does the client see?

   A. codes.Internal
   B. codes.Unknown with the raw message
   C. codes.Unavailable
   D. A panic

   **Answer: B.** Non-status errors become Unknown, which gives clients nothing to act on and can leak internals. Map errors to explicit codes with status.Error.

3. Why should a client set a deadline on every gRPC call?

   A. The deadline is sent to the server and propagates through downstream calls, so a stalled dependency can't hang the request forever
   B. gRPC rejects calls without one
   C. It enables compression
   D. It makes the call use HTTP/3

   **Answer: A.** gRPC transmits the deadline; the server's context is cancelled when it expires, and passing that context downstream bounds the whole chain.

4. Which situation fits gRPC least well?

   A. Go and Python services calling each other inside one system
   B. A service streaming status updates to another service
   C. Many internal services sharing a strict, versioned contract
   D. A public API mainly called from browsers by third-party developers

   **Answer: D.** Browsers can't speak native gRPC and third parties expect plain HTTP/JSON. Use REST there, or Connect/grpc-gateway if you also want gRPC internally.
