Skip to main content
skill CX Codex

go-grpc — Claude Code skill: gRPC services in Go

A skill that steers your coding agent through gRPC work in Go: proto contracts, error codes, deadlines, interceptors and streaming.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add eduardo-sl/go-agent-skills --skill go-grpc -a claude-code
Open on GitHub

What go-grpc does

The skill hands the agent a rulebook for gRPC in Go. Package names carry the version (payment.v1), field tags are never reused, and every RPC gets its own Request/Response pair. Generated code is committed, so builds don't drift with the toolchain, and buf lint plus buf breaking in CI catch renumbered fields before a client sees them.

Errors go back as status.Error, with domain errors mapped in one place: NotFound, AlreadyExists, InvalidArgument against FailedPrecondition, and Unavailable for retryable cases. Clients branch on the code from status.FromError, not on the message text. Every call carries a deadline, and ctx is passed into the database and downstream RPCs so cancellation actually propagates.

Cross-cutting work lives in interceptors: recovery outermost, then logging, then auth. Streaming RPCs use the matching StreamInterceptor variants. Tests run over bufconn with production interceptors and no network. The Go toolchain is required, and code generation needs protoc or buf — skip those and the build step fails.

What it helps with

  • design a proto contract with a versioned package
  • map domain errors onto gRPC status codes in one place
  • set deadlines and propagate cancellation downstream
  • move logging and auth into interceptors
  • test a service over bufconn without a network

Example prompts for the agent

  • Design the proto for a payments service with the version in the package name
  • Map domain errors to gRPC codes with status.Error
  • Write server streaming that exits on ctx.Done()

How to install go-grpc

1Skills CLI (Vercel)

npx skills add eduardo-sl/go-agent-skills --skill go-grpc -a claude-code

2Manually: put the files in place

~/.claude/skills/go-grpc/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/go-grpc/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about go-grpc

How do I install go-grpc in Claude Code?

Run npx skills add eduardo-sl/go-agent-skills --skill go-grpc -a claude-code. The skill lands at ~/.claude/skills/go-grpc/SKILL.md.

What does go-grpc do?

It gives the agent rules for building gRPC services in Go: proto design, status codes, deadlines, interceptors, streaming and bufconn tests.

What do I need for it to work?

The Go toolchain, plus protoc or buf for code generation. It targets Claude and similar agents on Go projects.

Source text

SKILL.md · MIT
---
name: go-grpc
description: >
  gRPC services in Go beyond the basics: proto design, status codes and
  error details, interceptors, deadlines, streaming, health checks, and
  graceful shutdown. Use when: "gRPC service", "proto design", "gRPC error
  handling", "interceptor", "gRPC streaming", "gRPC deadline", "grpc health
  check", "gRPC status codes".
  Not for: REST handlers (go-api-design), protobuf-agnostic layering
  (go-architecture-review), TLS hardening (go-security-audit).
user-invocable: true
license: MIT
compatibility: Designed for Claude Code or similar AI coding agents working on Go projects. Requires the Go toolchain. Requires protoc or buf for code generation.
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(gofmt:*) Bash(protoc:*) Bash(buf:*)
metadata:
  author: eduardo-sl
  version: "1.4.0"
---

# Go gRPC Services

gRPC's contract-first model only pays off if the contract is treated as
an API: versioned packages, deliberate error codes, deadlines
everywhere, and interceptors for everything cross-cutting.

Detailed reference material, loaded on demand:

- `references/clients.md` — `NewClient`, service-config retries and load
  balancing, keepalive on both sides, reading status codes.
- `references/testing.md` — in-memory `bufconn` server and client,
  asserting status codes.

Read a reference file only when the summary below is not enough.

## 1. Proto Design Rules

```protobuf
syntax = "proto3";

package payment.v1;                            // version IN the package
option go_package = "github.com/acme/payment-service/gen/payment/v1;paymentv1";

service PaymentService {
  rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse);
}

message CreatePaymentRequest {                 // one request/response pair
  string order_id = 1;                         // per RPC, always — even if
  int64 amount_cents = 2;                      // empty today
}

message CreatePaymentResponse {
  Payment payment = 1;
}
```

- Version in the package (`payment.v1`); breaking change = `payment.v2`.
- Never reuse or renumber field tags; `reserved 3, 7;` deleted ones.
- Dedicated Request/Response messages per RPC — adding a field later is
  free; changing a shared message breaks every RPC using it.
- Generate with buf or a pinned protoc in `make generate`; commit
  generated code so builds don't depend on toolchain drift.
- In CI, `buf lint` and `buf breaking --against '.git#branch=main'` stop
  renumbered fields and removed RPCs before they reach a client.
  Field rules (lengths, ranges, formats) belong in protovalidate
  annotations, enforced by an interceptor.

## 2. Errors: Status Codes, Not Strings

Return `status.Error`, mapping domain errors in ONE place:

```go
func (s *Server) CreatePayment(ctx context.Context, req *pb.CreatePaymentRequest) (*pb.CreatePaymentResponse, error) {
    p, err := s.svc.Create(ctx, toDomain(req))
    if err != nil {
        return nil, toStatus(err)
    }
    return &pb.CreatePaymentResponse{Payment: fromDomain(p)}, nil
}

func toStatus(err error) error {
    switch {
    case errors.Is(err, domain.ErrNotFound):
        return status.Error(codes.NotFound, "payment not found")
    case errors.Is(err, domain.ErrDuplicate):
        return status.Error(codes.AlreadyExists, "payment already exists")
    case errors.Is(err, context.DeadlineExceeded):
        return status.Error(codes.DeadlineExceeded, "timed out")
    default:
        return status.Error(codes.Internal, "internal error") // no details leak
    }
}
```

Code semantics that matter: `InvalidArgument` (bad request regardless
of state), `FailedPrecondition` (bad state), `NotFound`, `AlreadyExists`,
`Unauthenticated` vs `PermissionDenied`, `Unavailable` (retryable),
`Internal` (bug). Clients read codes with `status.FromError(err)` —
never parse messages.

## 3. Deadlines Are Mandatory

```go
// Client — every call gets a deadline
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
resp, err := client.CreatePayment(ctx, req)

// Server — check before expensive work
if err := ctx.Err(); err != nil {
    return nil, status.FromContextError(err).Err()
}
```

The server inherits the client's deadline through the context. Pass
`ctx` into every downstream call (DB, other RPCs) so cancellation
propagates end to end.

## 4. Interceptors for Cross-Cutting Concerns

Handlers stay business-only; recovery, auth, logging, metrics live in
interceptors:

```go
srv := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        recoveryInterceptor,   // outermost: panic → codes.Internal
        loggingInterceptor,
        authInterceptor,
    ),
)

func loggingInterceptor(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",
        slog.String("method", info.FullMethod),
        slog.Duration("duration", time.Since(start)),
        slog.String("code", status.Code(err).String()),
    )
    return resp, err
}
```

Order matters: recovery first (outermost), then observability, then
auth. Streaming RPCs need the parallel `StreamInterceptor` versions.

## 5. Streaming

- **Server streaming** for large result sets: `stream.Send` in a loop,
  return non-nil error to abort with a status.
- **Client/bidi streaming** only when the protocol truly needs it —
  each open stream holds a goroutine and flow-control state.
- Always terminate on `ctx.Done()`:

```go
func (s *Server) WatchPayments(req *pb.WatchRequest, stream pb.PaymentService_WatchPaymentsServer) error {
    for {
        select {
        case <-stream.Context().Done():
            return status.FromContextError(stream.Context().Err()).Err()
        case ev := <-s.events:
            if err := stream.Send(toProto(ev)); err != nil {
                return err
            }
        }
    }
}
```

## 6. Clients

- `grpc.NewClient` (it replaces the deprecated `Dial`) once per target at
  startup; share the `*grpc.ClientConn` — it multiplexes and reconnects.
- Retries, timeouts and load balancing in the service config: retry
  `UNAVAILABLE` on idempotent methods only, and use `round_robin` (the
  default `pick_first` sends everything to one backend).
- Client keepalive pings must be allowed by the server's
  `EnforcementPolicy` (default `MinTime` 5 minutes), or the server closes
  the connection with `too_many_pings`.
- Branch on `status.Code(err)`, never on the message.

Examples: `references/clients.md`.

## 7. Testing

Test through the generated client over `bufconn`: a real server with the
production interceptors, no network. Dial `passthrough:///bufnet` with
`WithContextDialer` — `NewClient`'s default `dns` resolver would try to
resolve the name. Assert with `status.Code(err)`. Example:
`references/testing.md`.

## 8. Production Server Setup

```go
lis, err := net.Listen("tcp", cfg.Addr)
if err != nil {
    return fmt.Errorf("listen: %w", err)
}

srv := grpc.NewServer(
    grpc.Creds(creds),                // TLS; mTLS for service-to-service
    grpc.MaxRecvMsgSize(4<<20),       // explicit; size it to the largest valid request
    grpc.ChainUnaryInterceptor(...),
)
pb.RegisterPaymentServiceServer(srv, server)

healthSrv := health.NewServer() // grpc.health.v1 — load balancers need it
healthpb.RegisterHealthServer(srv, healthSrv)
reflection.Register(srv)        // grpcurl/debugging; gate on non-prod if policy requires

go func() {
    <-ctx.Done()
    stopped := make(chan struct{})
    go func() { srv.GracefulStop(); close(stopped) }()
    select {
    case <-stopped:                 // in-flight RPCs finished
    case <-time.After(10 * time.Second):
        srv.Stop()                  // force after grace period
    }
}()

return srv.Serve(lis)
```

For mTLS, the server's `tls.Config` sets `ClientCAs` and
`ClientAuth: tls.RequireAndVerifyClientCert`; handlers read the caller's
verified identity from `peer.FromContext(ctx)` rather than trusting
metadata.

## Verification Checklist

1. Proto packages versioned (`*.v1`); no tag reuse; `reserved` for removals
2. Dedicated Request/Response message per RPC
3. Generated code produced by a pinned tool (buf/protoc) and committed; `buf breaking` runs in CI
4. All handler errors are `status.Error` with semantically correct codes
5. `codes.Internal` responses never leak internal error text
6. Every client call has a deadline; ctx propagated through all layers
7. Recovery, logging, auth implemented as chained interceptors (unary + stream)
8. Streams select on `stream.Context().Done()`
9. Health service registered; graceful stop with forced fallback; TLS (mTLS internally) and an explicit receive limit
10. `grpcurl` smoke test (or generated client test) passes against the running server
11. One shared `ClientConn` per target, built with `grpc.NewClient`; retries limited to `UNAVAILABLE`

© eduardo-sl · MIT · eduardo-sl/go-agent-skills

Similar skills

All skills for Backend and APIs →

From the same set11

git-commit A skill that makes your agent write commit messages in Conventional Commits style: typed, scoped, imperative. Useful when the repo history has turned into noise. CX Codex +1 ★ 83repo stars go-troubleshooting A skill that walks the agent through Go runtime diagnosis: panics, deadlocks, goroutine and memory leaks, race reports. Useful when a program crashes or hangs and the cause is unclear. CX Codex +1 ★ 83repo stars go-observability A skill that sets conventions for Go service telemetry: structured logs with log/slog, OpenTelemetry traces and metrics, SLO alerts. Useful once a service runs in production and you need to see what it does. CX Codex +1 ★ 83repo stars go-error-handling A skill that tells your agent how to create, wrap and inspect Go errors: %w wrapping, sentinel errors, custom types, errors.Is and errors.As. CX Codex +1 ★ 83repo stars go-context A skill that makes your coding agent handle context.Context in Go correctly: cancellation, timeouts, values and the usual anti-patterns. CX Codex +1 ★ 83repo stars go-project-layout Picks a folder layout for a new Go project: cmd/, internal/, or a flat root for small tools. Useful at the start, before the structure hardens. CX Codex +1 ★ 83repo stars go-dependency-injection A skill that steers your agent toward constructor injection and a single composition root in main. Useful once globals creep in or tests get hard to write. CX Codex +1 ★ 83repo stars go-api-design A skill that steers the agent through Go HTTP handler design: routing, middleware, validation, pagination, graceful shutdown. Useful when you start a REST service or rework one. CX Codex +1 ★ 83repo stars

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor