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.
npx skills add eduardo-sl/go-agent-skills --skill go-grpc -a claude-codeWhat 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
bufconnwithout 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-code2Manually: put the files in place
~/.claude/skills/go-grpc/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/go-grpc/
1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-grpc -a codex2Manually: put the files in place
.agents/skills/go-grpc/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-grpc -a cursor2Manually: put the files in place
.cursor/skills/go-grpc/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install eduardo-sl/go-agent-skills go-grpc2Manually: put the files in place
.github/skills/go-grpc/SKILL.md1Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-grpc -a gemini-cli2Platform CLI
gemini skills install https://github.com/eduardo-sl/go-agent-skills --path 'skills/(architecture)/go-grpc'Manually: put the files in place
.devin/skills/go-grpc/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add eduardo-sl/go-agent-skills --skill go-grpc -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
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