home / skills / bobmatnyc / claude-mpm-skills / grpc

This skill helps you design and implement production-ready Go gRPC services with versioned protos, deadlines, TLS, interceptors, health checks, and testing.

npx playbooks add skill bobmatnyc/claude-mpm-skills --skill grpc

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
8.3 KB
---
name: golang-grpc
description: "Production gRPC in Go: protobuf layout, codegen, interceptors, deadlines, error codes, streaming, health checks, TLS, and testing with bufconn"
version: 1.0.0
category: toolchain
author: Claude MPM Team
license: MIT
progressive_disclosure:
  entry_point:
    summary: "Build production gRPC services in Go with protobuf-first APIs, interceptors, deadlines, status codes, and streaming patterns"
    when_to_use: "When building Go microservices that need typed RPC, streaming, low latency, or strong client/server contracts with protobuf"
    quick_start: "1. Define versioned protos (pkg v1) 2. Generate Go code 3. Implement server + interceptors 4. Enforce deadlines 5. Test with bufconn"
  token_estimate:
    entry: 160
    full: 6500
context_limit: 900
tags:
  - golang
  - grpc
  - protobuf
  - microservices
  - streaming
  - interceptors
  - observability
requires_tools: []
---

# Go gRPC (Production)

## Overview

gRPC provides strongly-typed RPC APIs backed by Protocol Buffers, with first-class streaming support and excellent performance for service-to-service communication. This skill focuses on production defaults: versioned protos, deadlines, error codes, interceptors, health checks, TLS, and testability.

## Quick Start

### 1) Define a versioned protobuf API

✅ **Correct: versioned package**
```proto
// proto/users/v1/users.proto
syntax = "proto3";

package users.v1;
option go_package = "example.com/myapp/gen/users/v1;usersv1";

service UsersService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  rpc ListUsers(ListUsersRequest) returns (stream User);
}

message GetUserRequest { string id = 1; }
message GetUserResponse { User user = 1; }
message ListUsersRequest { int32 page_size = 1; string page_token = 2; }

message User {
  string id = 1;
  string email = 2;
  string display_name = 3;
}
```

❌ **Wrong: unversioned package (hard to evolve)**
```proto
package users;
```

### 2) Generate Go code

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

Generate:
```bash
protoc -I proto \
  --go_out=./gen --go_opt=paths=source_relative \
  --go-grpc_out=./gen --go-grpc_opt=paths=source_relative \
  proto/users/v1/users.proto
```

### 3) Implement server with deadlines and status codes

✅ **Correct: validate + map errors to gRPC codes**
```go
package usersvc

import (
    "context"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    usersv1 "example.com/myapp/gen/users/v1"
)

type Service struct {
    usersv1.UnimplementedUsersServiceServer
    Repo Repo
}

type Repo interface {
    GetUser(ctx context.Context, id string) (User, error)
}

type User struct {
    ID, Email, DisplayName string
}

func (s *Service) GetUser(ctx context.Context, req *usersv1.GetUserRequest) (*usersv1.GetUserResponse, error) {
    if req.GetId() == "" {
        return nil, status.Error(codes.InvalidArgument, "id is required")
    }

    u, err := s.Repo.GetUser(ctx, req.GetId())
    if err != nil {
        if err == ErrNotFound {
            return nil, status.Error(codes.NotFound, "user not found")
        }
        return nil, status.Error(codes.Internal, "internal error")
    }

    return &usersv1.GetUserResponse{
        User: &usersv1.User{
            Id:          u.ID,
            Email:       u.Email,
            DisplayName: u.DisplayName,
        },
    }, nil
}
```

❌ **Wrong: return raw errors (clients lose code semantics)**
```go
return nil, errors.New("user not found")
```

## Core Concepts

### Deadlines and cancellation

Make every call bounded; enforce server-side timeouts for expensive handlers.

✅ **Correct: require deadline**
```go
if _, ok := ctx.Deadline(); !ok {
    return nil, status.Error(codes.InvalidArgument, "deadline required")
}
```

### Metadata

Use metadata for auth/session correlation, not for primary request data.

✅ **Correct: read auth token from metadata**
```go
md, _ := metadata.FromIncomingContext(ctx)
auth := ""
if vals := md.Get("authorization"); len(vals) > 0 {
    auth = vals[0]
}
```

## Interceptors (Middleware)

Use interceptors for cross-cutting concerns: auth, logging, metrics, tracing, request IDs.

✅ **Correct: unary interceptor with request ID**
```go
func unaryRequestID() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        id := uuid.NewString()
        ctx = context.WithValue(ctx, requestIDKey{}, id)
        resp, err := handler(ctx, req)
        return resp, err
    }
}
```

## Streaming patterns

### Server streaming (paginate or stream results)

✅ **Correct: stop on ctx.Done()**
```go
func (s *Service) ListUsers(req *usersv1.ListUsersRequest, stream usersv1.UsersService_ListUsersServer) error {
    users, err := s.Repo.ListUsers(stream.Context(), int(req.GetPageSize()))
    if err != nil {
        return status.Error(codes.Internal, "internal error")
    }

    for _, u := range users {
        select {
        case <-stream.Context().Done():
            return stream.Context().Err()
        default:
        }

        if err := stream.Send(&usersv1.User{
            Id:          u.ID,
            Email:       u.Email,
            DisplayName: u.DisplayName,
        }); err != nil {
            return err
        }
    }
    return nil
}
```

### Unary vs streaming decision

- Use **unary** for single request/response and simple retries.
- Use **server streaming** for large result sets or continuous updates.
- Use **client streaming** for bulk uploads with one final response.
- Use **bidirectional streaming** for interactive protocols.

## Production Hardening

### Health checks and reflection

Add health service; enable reflection only in non-production environments.

✅ **Correct: health + conditional reflection**
```go
hs := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, hs)

if env != "production" {
    reflection.Register(s)
}
```

### Graceful shutdown

Prefer `GracefulStop` with a deadline.

✅ **Correct: graceful stop**
```go
stopped := make(chan struct{})
go func() {
    grpcServer.GracefulStop()
    close(stopped)
}()

select {
case <-stopped:
case <-time.After(10 * time.Second):
    grpcServer.Stop()
}
```

### TLS

Use TLS (or mTLS) in production; avoid insecure credentials outside local dev.

✅ **Correct: server TLS**
```go
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil { return err }

grpcServer := grpc.NewServer(grpc.Creds(creds))
```

## Testing (bufconn)

Test gRPC handlers without opening real sockets using `bufconn`.

✅ **Correct: in-memory gRPC test server**
```go
const bufSize = 1024 * 1024

lis := bufconn.Listen(bufSize)
srv := grpc.NewServer()
usersv1.RegisterUsersServiceServer(srv, &Service{Repo: repo})

go func() { _ = srv.Serve(lis) }()

ctx := context.Background()
conn, err := grpc.DialContext(
    ctx,
    "bufnet",
    grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }),
    grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil { t.Fatal(err) }
defer conn.Close()

client := usersv1.NewUsersServiceClient(conn)
resp, err := client.GetUser(ctx, &usersv1.GetUserRequest{Id: "1"})
_ = resp
_ = err
```

## Anti-Patterns

- **Ignore deadlines**: unbounded handlers cause tail latency and resource exhaustion.

- **Return string errors**: map domain errors to `codes.*` with `status.Error` or `status.Errorf`.

- **Stream without backpressure**: stop on `ctx.Done()` and handle `Send` errors.

- **Expose reflection in production**: treat reflection as a discovery surface.

## Troubleshooting

### Symptom: clients see `UNKNOWN` errors

Actions:
- Return `status.Error(codes.X, "...")` instead of raw errors.
- Wrap domain errors into typed errors, then map to gRPC codes.

### Symptom: slow/hanging requests

Actions:
- Require deadlines and propagate `ctx` to downstream calls.
- Add server-side timeouts and bounded concurrency in repositories.

### Symptom: flaky streaming

Actions:
- Stop streaming on `ctx.Done()` and handle `stream.Send` errors.
- Avoid buffering entire result sets before sending.

## Resources

- gRPC Go: https://github.com/grpc/grpc-go
- Protobuf Go: https://pkg.go.dev/google.golang.org/protobuf
- gRPC error codes: https://grpc.io/docs/guides/error/

Overview

This skill teaches production-ready gRPC in Go with practical defaults: versioned protobuf layouts, code generation, interceptors, deadlines, error mapping, streaming patterns, health checks, TLS, and in-memory testing with bufconn. It focuses on conventions and anti-patterns that make services robust, observable, and evolvable. Applied examples and troubleshooting tips help you harden real services quickly.

How this skill works

The skill inspects common gRPC server and client patterns and provides concrete implementations and recommendations. It covers proto package versioning and go_package layout, generating Go stubs with protoc plugins, mapping domain errors to gRPC status codes, using interceptors for cross-cutting concerns, enforcing deadlines and cancellation, secure transport with TLS, and running tests with bufconn to avoid real sockets.

When to use it

  • Building new service-to-service APIs in Go that must be stable and versionable
  • Implementing robust server behavior: deadlines, graceful shutdown, and health checks
  • Adding cross-cutting middleware: auth, logging, tracing, request IDs
  • Designing streaming endpoints for large result sets or interactive protocols
  • Securing production services with TLS/mTLS and avoiding reflection exposure
  • Writing unit and integration tests without network sockets using bufconn

Best practices

  • Version every proto package; include go_package option with module path and semver in the layout
  • Always validate inputs and map domain errors to grpc/codes using status.Error or status.Errorf
  • Require and respect ctx.Deadline(); propagate ctx to downstream calls and enforce server-side timeouts
  • Use interceptors for authentication, metrics, tracing, and to inject request IDs consistently
  • Choose unary vs streaming based on size and interaction pattern; stop streaming promptly on ctx.Done()
  • Enable health service and use reflection only in non-production environments; prefer GracefulStop with a bounded timeout

Example use cases

  • A user service with versioned proto packages and generated Go code served over TLS
  • A middleware stack adding request IDs, auth token extraction from metadata, and centralized logging
  • Server-streaming endpoint that paginates results and stops on client cancellation
  • Tests that spin up an in-memory gRPC server using bufconn to validate handlers without opening network ports
  • Graceful shutdown flow that attempts GracefulStop then forces Stop after a deadline

FAQ

Why version protobuf packages?

Versioned proto packages allow safe evolution of APIs without breaking older clients; include a versioned directory and go_package option for clear generator output.

How should I return errors from handlers?

Map errors to grpc codes using status.Error or status.Errorf. Returning raw errors yields UNKNOWN on the client and loses semantic meaning.