{"id":17782,"plugin_id":"plugins_6a7b1e3e30948191aea92f131b0f6ca9","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:14:21.102Z","digest":"eab2d266bc12ee8f8754ecd8405621f1ad6f76524cdeaa7e253b3e1dc8c2fcfc","against":null,"payload":{"description":"Use when implementing or reviewing gRPC servers/clients in Go. Covers .proto organisation, code generation with protoc/buf, server bootstrap (interceptors, health, graceful shutdown), client patterns (reuse, deadlines, retries), status.Code error handling, streaming, TLS/mTLS, and bufconn testing. Apply when writing .proto files, adding interceptors, or auditing a service for production readiness.","included_files":[{"relative_path":"agents/openai.yaml","size_in_bytes":197},{"relative_path":"references/anti-patterns.md","size_in_bytes":3948},{"relative_path":"references/proto-and-codegen.md","size_in_bytes":3901},{"relative_path":"references/status-and-errors.md","size_in_bytes":4171},{"relative_path":"references/testing.md","size_in_bytes":3619}],"name":"go-grpc","skill_md_contents":"---\nname: go-grpc\ndescription: \"Use when implementing or reviewing gRPC servers/clients in Go. Covers .proto organisation, code generation with protoc/buf, server bootstrap (interceptors, health, graceful shutdown), client patterns (reuse, deadlines, retries), status.Code error handling, streaming, TLS/mTLS, and bufconn testing. Apply when writing .proto files, adding interceptors, or auditing a service for production readiness.\"\nlicense: MIT\ncompatibility: \"Designed for Claude Code or similar AI coding agents. Requires Go 1.21+, protoc (or buf), and google.golang.org/grpc v1.60+.\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)\n---\n\n# Go gRPC\n\nTreat gRPC as a transport. Keep `.proto`-generated code and business logic separated. The official Go implementation is `google.golang.org/grpc`; pair it with `protoc-gen-go` + `protoc-gen-go-grpc` (or `buf generate`).\n\n## Core Rules\n\n1. **One concern per layer.** `.proto` defines the contract; generated code lives in `gen/`; service implementation lives in `internal/`. Never edit generated files.\n2. **Always wrap RPC arguments in Request/Response messages.** Bare scalars (`string`, `int32`) cannot be evolved without breaking callers.\n3. **Return typed status codes, never raw errors.** A `fmt.Errorf` becomes `codes.Unknown` on the wire — the client cannot decide whether to retry.\n4. **Every client call has a deadline.** No `context.Background()` to a remote service. Set `context.WithTimeout` per call.\n5. **Reuse connections.** HTTP/2 multiplexes; creating a new `grpc.ClientConn` per request is a TLS handshake leak.\n6. **Disable reflection in production.** Reflection is a developer convenience that doubles as an API enumeration tool for attackers.\n\n## When to Use What\n\n| Need | Use |\n|---|---|\n| Define service | `.proto` file in `proto/<service>/v1/` |\n| Generate stubs | `buf generate` or `protoc --go_out --go-grpc_out` |\n| Cross-cutting (auth, logging, recovery) | `grpc.ChainUnaryInterceptor` / `ChainStreamInterceptor` |\n| Health probes (Kubernetes) | `grpc_health_v1` from `google.golang.org/grpc/health` |\n| Errors with details | `status.Errorf(codes.X, ...)` + `WithDetails(errdetails.BadRequest{...})` |\n| Tests | `google.golang.org/grpc/test/bufconn` |\n| Service mesh / mTLS | `credentials.NewTLS` or delegate to Istio/Linkerd |\n\n> Read [references/proto-and-codegen.md](references/proto-and-codegen.md) when organizing `.proto` packages or wiring `buf`.\n> Read [references/status-and-errors.md](references/status-and-errors.md) when mapping domain errors to gRPC codes.\n\n## Server Bootstrap\n\n```go\nimport (\n    \"google.golang.org/grpc\"\n    \"google.golang.org/grpc/health\"\n    healthpb \"google.golang.org/grpc/health/grpc_health_v1\"\n)\n\nsrv := grpc.NewServer(\n    grpc.ChainUnaryInterceptor(recoveryUnary, loggingUnary, authUnary),\n    grpc.ChainStreamInterceptor(recoveryStream, loggingStream),\n)\npb.RegisterUserServiceServer(srv, &userService{...})\nhealthpb.RegisterHealthServer(srv, health.NewServer())\n\ngo func() { _ = srv.Serve(lis) }()\n\n// Graceful shutdown bounded by a hard timeout.\n<-shutdownSignal\nstopped := make(chan struct{})\ngo func() { srv.GracefulStop(); close(stopped) }()\nselect {\ncase <-stopped:\ncase <-time.After(15 * time.Second):\n    srv.Stop()\n}\n```\n\nThree pieces are non-negotiable: interceptors for cross-cutting concerns, health service for Kubernetes probes, and a bounded graceful shutdown.\n\n## Client Bootstrap\n\n```go\nconn, _ := grpc.NewClient(\"dns:///user-service:50051\",\n    grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)),\n    grpc.WithDefaultServiceConfig(`{\n      \"loadBalancingPolicy\": \"round_robin\",\n      \"methodConfig\": [{\n        \"name\": [{\"service\": \"user.v1.UserService\"}],\n        \"timeout\": \"5s\",\n        \"retryPolicy\": {\n          \"maxAttempts\": 3, \"initialBackoff\": \"0.1s\", \"maxBackoff\": \"1s\",\n          \"backoffMultiplier\": 2, \"retryableStatusCodes\": [\"UNAVAILABLE\"]\n        }\n      }]\n    }`),\n)\nclient := pb.NewUserServiceClient(conn)\nctx, cancel := context.WithTimeout(context.Background(), 2*time.Second); defer cancel()\nresp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: id})\n```\n\nThe service config is the right place for retries — let the library handle the loop, backoff, and `UNAVAILABLE`-only filter.\n\n## Errors\n\nA raw Go error returned from an RPC becomes `codes.Unknown`. The client cannot tell a 404 from a 500. Always use `status.Errorf`:\n\n```go\nif errors.Is(err, ErrNotFound) {\n    return nil, status.Errorf(codes.NotFound, \"user %q not found\", req.Id)\n}\nif errors.As(err, &validationErr) {\n    st, _ := status.New(codes.InvalidArgument, \"validation\").WithDetails(\n        &errdetails.BadRequest{FieldViolations: violations(validationErr)},\n    )\n    return nil, st.Err()\n}\nreturn nil, status.Errorf(codes.Internal, \"lookup: %v\", err)\n```\n\nQuick map:\n\n| Domain | Code |\n|---|---|\n| Missing/invalid field | `InvalidArgument` |\n| Not found | `NotFound` |\n| Already exists | `AlreadyExists` |\n| Unauthenticated | `Unauthenticated` |\n| Authenticated but forbidden | `PermissionDenied` |\n| Rate-limited | `ResourceExhausted` |\n| Dependency down, retriable | `Unavailable` |\n| Bug, unexpected | `Internal` |\n\n## Streaming\n\n| Pattern | Use case |\n|---|---|\n| Server streaming | Log tailing, paginated result sets, server-sent events |\n| Client streaming | File upload, batch ingest |\n| Bidirectional | Chat, real-time sync |\n\nStreams must respect `ctx.Done()`. A goroutine reading from a stream after the client disconnects is a slow leak.\n\n```go\nfunc (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {\n    for _, u := range s.repo.All(stream.Context()) {\n        if err := stream.Send(toProto(u)); err != nil {\n            return err // includes ctx canceled\n        }\n    }\n    return nil\n}\n```\n\n## Testing with bufconn\n\n`bufconn` is an in-memory `net.Listener`. It exercises the real gRPC stack — interceptors, marshaling, metadata — without binding a TCP port. See [references/testing.md](references/testing.md) for the full harness plus table-driven status-code assertions, metadata injection, and stream testing.\n\n## Security Notes\n\n- TLS in production. Plaintext is only acceptable behind a confirmed-private network (and even then mTLS is preferable).\n- For service-to-service auth, prefer a mesh (Istio/Linkerd) over hand-rolled token validation.\n- For user auth, implement `credentials.PerRPCCredentials` to attach a token and validate inside an auth interceptor.\n- Reflection: enable in dev, disable in prod via build tag or env flag.\n\n## Anti-Patterns\n\n| Anti-pattern | Why it hurts | Do this instead |\n|---|---|---|\n| `return fmt.Errorf(\"not found\")` | Wire code is `Unknown`, clients can't retry-discriminate | `status.Errorf(codes.NotFound, ...)` |\n| `context.Background()` to a client call | No deadline → goroutines pile up on a slow dependency | `context.WithTimeout(parent, 5s)` |\n| New `ClientConn` per request | TLS handshake every call; sockets exhaust | One `grpc.NewClient` at startup, reuse |\n| Bare `string` as RPC argument | Cannot add fields without breaking callers | Always Request/Response messages |\n| Reflection on in production | Lets attackers enumerate every method | Compile-out with build tag in prod |\n| `codes.Internal` for all errors | Client retry config can't distinguish bugs from outages | Map domain → specific codes |\n| No health service | Kubernetes can't gate traffic; rolling deploys break | Register `grpc_health_v1` |\n| Ignoring `stream.Context().Done()` | Goroutines run after client disconnect | Select on `ctx.Done()` in stream loops |\n\n## Verification Checklist\n\n- [ ] `.proto` packages are versioned (`pkg/v1`, not `pkg`)\n- [ ] All RPCs take Request and return Response messages\n- [ ] Generated code is in a separate directory, never edited\n- [ ] Every error return uses `status.Errorf` with a specific code\n- [ ] Every client call has a deadline via `context.WithTimeout`\n- [ ] Server registers `grpc_health_v1`\n- [ ] `GracefulStop` is bounded by a `time.After` fallback\n- [ ] Reflection is gated to non-production builds\n- [ ] Tests use `bufconn` and assert `status.Code(err)`\n\n## References\n\n- [references/proto-and-codegen.md](references/proto-and-codegen.md) — `.proto` layout, `buf.yaml`, codegen flags\n- [references/status-and-errors.md](references/status-and-errors.md) — code mapping, rich details with `errdetails`\n- [references/testing.md](references/testing.md) — `bufconn`, metadata, streaming assertions\n- [references/anti-patterns.md](references/anti-patterns.md) — detailed walkthrough of each anti-pattern\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}