Generated banner for Golang Grpc
Generated logo for Golang Grpc

Golang Grpc

Samuel BertheCommunityCreated by samberDistributed by Samuel Berthe

  • Skill
  • Tool integration
  • Software development
  • MIT
  • No issues found

From the author

Provides gRPC usage guidelines, protobuf organization, and production-ready patterns for Golang microservices. Use when implementing, reviewing, or debugging gRPC servers/clients, writing proto files, setting up interceptors, handling gRPC errors with status codes, configuring TLS/mTLS, testing with bufconn, or working with streaming RPCs.
— the description by samber

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a comprehensive framework for building robust gRPC microservices in Go. It covers the full lifecycle of gRPC development, including protobuf organization, server and client implementation patterns, error handling, security configurations, and testing strategies. It emphasizes production-grade practices such as graceful shutdowns, interceptor usage, proper status code mapping, and connection management to ensure reliable and maintainable distributed systems.

What it does

  • Provides best practices for organizing protobuf files and generating Go code.
  • Offers patterns for implementing gRPC servers with health checks and interceptors.
  • Defines standard approaches for client-side connection management and load balancing.
  • Guides developers on mapping Go errors to appropriate gRPC status codes.
  • Includes techniques for testing gRPC services using in-memory bufconn connections.
  • Outlines security configurations including TLS/mTLS and production hardening.

When to use it

  • When designing or implementing a new gRPC service in Go.
  • When auditing existing gRPC codebases for correctness and operability.
  • When debugging gRPC communication issues or performance bottlenecks.
  • When setting up testing suites for gRPC clients and servers.

Advantages

  • Promotes consistent, production-ready gRPC architecture.
  • Reduces common pitfalls like connection leaks and improper error handling.
  • Facilitates easier testing of complex gRPC stacks without network overhead.
  • Encourages clean separation between transport layers and business logic.

Usage examples

“How do I implement a graceful shutdown for my gRPC server in Go?”
The agent provides a pattern using GracefulStop() with a timeout fallback to Stop() to ensure in-flight requests are handled correctly.
“Show me how to test my gRPC service using bufconn.”
The agent generates a test setup using bufconn to create an in-memory connection, allowing full stack testing without network dependencies.
“What is the best way to handle errors in my gRPC service?”
The agent explains how to use status.Errorf with specific codes like NotFound or InvalidArgument instead of returning raw errors.
“How should I organize my proto files for a Go project?”
The agent recommends a domain-based, versioned directory structure (e.g., proto/user/v1/) and the use of Request/Response wrapper messages.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/samber/golang-grpc/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip golang-grpc.zip -d .agents/skills

Documentation by samber

Persona: You are a Go distributed systems engineer. You design gRPC services for correctness and operability — proper status codes, deadlines, interceptors, and graceful shutdown matter as much as the happy path.

Modes:

  • Build mode — implementing a new gRPC server or client from scratch.

  • Review mode — auditing existing gRPC code for correctness, security, and operability issues.

Dependencies:

  • protoc: brew install protobuf

  • protoc-gen-go: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

  • protoc-gen-go-grpc: go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Go gRPC Best Practices

Treat gRPC as a pure transport layer — keep it separate from business logic. The official Go implementation is google.golang.org/grpc.

This skill is not exhaustive — refer to library documentation and code examples for more information:

  • For Go package docs, symbols, versions, importers, and known vulnerabilities, → See samber/cc-skills-golang@golang-pkg-go-dev skill (godig), preferred over Context7 for Go package facts.

  • To navigate this library's usage in your own code (definitions, call sites, diagnostics), → See samber/cc-skills-golang@golang-gopls skill (gopls).

  • Context7 remains a fallback for docs not indexed on pkg.go.dev.

Quick Reference

ConcernPackage / Tool
Service definitionprotoc or buf with .proto files
Code generationprotoc-gen-go, protoc-gen-go-grpc
Error handlinggoogle.golang.org/grpc/status with codes
Rich error detailsgoogle.golang.org/genproto/googleapis/rpc/errdetails
Interceptorsgrpc.ChainUnaryInterceptor, grpc.ChainStreamInterceptor
Middleware ecosystemgithub.com/grpc-ecosystem/go-grpc-middleware
Testinggoogle.golang.org/grpc/test/bufconn
TLS / mTLSgoogle.golang.org/grpc/credentials
Health checksgoogle.golang.org/grpc/health

Proto File Organization

Organize by domain with versioned directories (proto/user/v1/). Always use Request/Response wrapper messages — bare types like string cannot have fields added later. Generate with buf generate or protoc.

Proto & code generation reference

Server Implementation

  • Implement health check service (grpc_health_v1) — Kubernetes probes need it to determine readiness

  • Use interceptors for cross-cutting concerns (logging, auth, recovery) — keeps business logic clean

  • Use GracefulStop() with a timeout fallback to Stop() — drains in-flight RPCs while preventing hangs

  • Disable reflection in production — it exposes your full API surface

srv := grpc.NewServer(
    grpc.ChainUnaryInterceptor(loggingInterceptor, recoveryInterceptor),
)
pb.RegisterUserServiceServer(srv, svc)
healthpb.RegisterHealthServer(srv, health.NewServer())

go srv.Serve(lis)

// On shutdown signal:
stopped := make(chan struct{})
go func() { srv.GracefulStop(); close(stopped) }()
select {
case <-stopped:
case <-time.After(15 * time.Second):
    srv.Stop()
}
Interceptor Pattern
func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("method=%s duration=%s code=%s", info.FullMethod, time.Since(start), status.Code(err))
    return resp, err
}

Client Implementation

  • Reuse connections — gRPC multiplexes RPCs on a single HTTP/2 connection; one-per-request wastes TCP/TLS handshakes

  • Set deadlines on every call (context.WithTimeout) — without one, a slow upstream hangs goroutines indefinitely

  • Use round_robin with headless Kubernetes services via dns:/// scheme

  • Pass metadata (auth tokens, trace IDs) via metadata.NewOutgoingContext

conn, err := grpc.NewClient("dns:///user-service:50051",
    grpc.WithTransportCredentials(creds),
    grpc.WithDefaultServiceConfig(`{
        "loadBalancingPolicy": "round_robin",
        "methodConfig": [{
            "name": [{"service": ""}],
            "timeout": "5s",
            "retryPolicy": {
                "maxAttempts": 3,
                "initialBackoff": "0.1s",
                "maxBackoff": "1s",
                "backoffMultiplier": 2,
                "retryableStatusCodes": ["UNAVAILABLE"]
            }
        }]
    }`),
)
client := pb.NewUserServiceClient(conn)

Error Handling

Always return gRPC errors using status.Error with a specific code — a raw error becomes codes.Unknown, telling the client nothing actionable. Clients use codes to decide retry vs fail-fast vs degrade.

CodeWhen to Use
InvalidArgumentMalformed input (missing field, bad format)
NotFoundEntity does not exist
AlreadyExistsCreate failed, entity exists
PermissionDeniedCaller lacks permission
UnauthenticatedMissing or invalid token
FailedPreconditionSystem not in required state
ResourceExhaustedRate limit or quota exceeded
UnavailableTransient issue, safe to retry
InternalUnexpected bug
DeadlineExceededTimeout
// ✗ Bad — caller gets codes.Unknown, can't decide whether to retry
return nil, fmt.Errorf("user not found")

// ✓ Good — specific code lets clients act appropriately
if errors.Is(err, ErrNotFound) {
    return nil, status.Errorf(codes.NotFound, "user %q not found", req.UserId)
}
return nil, status.Errorf(codes.Internal, "lookup failed: %v", err)

For field-level validation errors, attach errdetails.BadRequest via status.WithDetails.

Streaming

PatternUse Case
Server streamingServer sends a sequence (log tailing, result sets)
Client streamingClient sends a sequence, server responds once (file upload, batch)
BidirectionalBoth send independently (chat, real-time sync)

Prefer streaming over large single messages — avoids per-message size limits and lowers memory pressure.

func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    for _, u := range users {
        if err := stream.Send(u); err != nil {
            return err
        }
    }
    return nil
}

Testing

Use bufconn for in-memory connections that exercise the full gRPC stack (serialization, interceptors, metadata) without network overhead. Always test that error scenarios return the expected gRPC status codes.

Testing patterns and examples

Security

  • TLS MUST be enabled in production — credentials travel in metadata

  • For service-to-service auth, use mTLS or delegate to a service mesh (Istio, Linkerd)

  • For user auth, implement credentials.PerRPCCredentials and validate tokens in an auth interceptor

  • Reflection SHOULD be disabled in production to prevent API discovery

Performance

SettingPurposeTypical Value
keepalive.ServerParameters.TimePing interval for idle connections30s
keepalive.ServerParameters.TimeoutPing ack timeout10s
grpc.MaxRecvMsgSizeOverride 4 MB default for large payloads16 MB
Connection poolingMultiple conns for high-load streaming4 connections

Most services do not need connection pooling — profile before adding complexity.

Common Mistakes

MistakeFix
Returning raw errorBecomes codes.Unknown — client can't decide whether to retry. Use status.Errorf with a specific code
No deadline on client callsSlow upstream hangs indefinitely. Always context.WithTimeout
New connection per requestWastes TCP/TLS handshakes. Create once, reuse — HTTP/2 multiplexes RPCs
Reflection enabled in productionLets attackers enumerate every method. Enable only in dev/staging
codes.Internal for all errorsWrong codes break client retry logic. Unavailable triggers retry; InvalidArgument does not
Bare types as RPC argumentsCan't add fields to string. Wrapper messages allow backwards-compatible evolution
Missing health check serviceKubernetes can't determine readiness, kills pods during deployments
Ignoring context cancellationLong operations continue after caller gave up. Check ctx.Err()

Cross-References

  • → See samber/cc-skills-golang@golang-context skill for deadline and cancellation patterns

  • → See samber/cc-skills-golang@golang-error-handling skill for gRPC error to Go error mapping

  • → See samber/cc-skills-golang@golang-observability skill for gRPC interceptors (logging, tracing, metrics)

  • → See samber/cc-skills-golang@golang-testing skill for gRPC testing with bufconn

Files

references/testing.md

gRPC Testing Reference

Table of Contents

  • Testing with bufconn

    • Basic Setup

    • Setup with Interceptors

  • Testing Error Codes

    • Table-Driven Error Code Tests

  • Testing Streaming RPCs

    • Server Streaming

    • Client Streaming

  • Testing Metadata

  • Testing Deadlines

  • Integration Test Patterns

Testing with bufconn

bufconn creates in-memory connections that exercise the full gRPC stack (serialization, interceptors, metadata) without network overhead. This is the standard approach for gRPC unit and integration tests.

Basic Setup
func setupTest(t *testing.T) pb.UserServiceClient {
    t.Helper()
    lis := bufconn.Listen(1024 * 1024)
    t.Cleanup(func() { lis.Close() })

    srv := grpc.NewServer()
    pb.RegisterUserServiceServer(srv, newTestService())
    t.Cleanup(func() { srv.Stop() })
    go srv.Serve(lis)

    conn, err := grpc.NewClient("passthrough:///bufconn",
        grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
            return lis.DialContext(ctx)
        }),
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        t.Fatalf("dial: %v", err)
    }
    t.Cleanup(func() { conn.Close() })
    return pb.NewUserServiceClient(conn)
}
Setup with Interceptors

Test your interceptors by including them in the test server:

func setupTestWithInterceptors(t *testing.T) pb.UserServiceClient {
    t.Helper()
    lis := bufconn.Listen(1024 * 1024)
    t.Cleanup(func() { lis.Close() })

    srv := grpc.NewServer(
        grpc.ChainUnaryInterceptor(
            loggingInterceptor,
            authInterceptor,
            recoveryInterceptor,
        ),
    )
    pb.RegisterUserServiceServer(srv, newTestService())
    t.Cleanup(func() { srv.Stop() })
    go srv.Serve(lis)

    conn, err := grpc.NewClient("passthrough:///bufconn",
        grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
            return lis.DialContext(ctx)
        }),
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        t.Fatalf("dial: %v", err)
    }
    t.Cleanup(func() { conn.Close() })
    return pb.NewUserServiceClient(conn)
}

Testing Error Codes

Always verify that RPCs return the expected gRPC status codes — clients rely on codes for retry and error-handling logic.

func TestGetUser_NotFound(t *testing.T) {
    client := setupTest(t)
    _, err := client.GetUser(context.Background(), &pb.GetUserRequest{UserId: "nonexistent"})

    st, ok := status.FromError(err)
    if !ok {
        t.Fatalf("expected gRPC status error, got: %v", err)
    }
    if st.Code() != codes.NotFound {
        t.Errorf("expected NotFound, got %s: %s", st.Code(), st.Message())
    }
}
Table-Driven Error Code Tests
func TestGetUser_Errors(t *testing.T) {
    client := setupTest(t)

    tests := []struct {
        name     string
        req      *pb.GetUserRequest
        wantCode codes.Code
    }{
        {
            name:     "empty user ID",
            req:      &pb.GetUserRequest{UserId: ""},
            wantCode: codes.InvalidArgument,
        },
        {
            name:     "user not found",
            req:      &pb.GetUserRequest{UserId: "nonexistent"},
            wantCode: codes.NotFound,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := client.GetUser(context.Background(), tt.req)
            st, ok := status.FromError(err)
            if !ok {
                t.Fatalf("expected gRPC status error, got: %v", err)
            }
            if st.Code() != tt.wantCode {
                t.Errorf("code = %s, want %s; message: %s", st.Code(), tt.wantCode, st.Message())
            }
        })
    }
}

Testing Streaming RPCs

Server Streaming
func TestListUsers_Stream(t *testing.T) {
    client := setupTest(t)
    stream, err := client.ListUsers(context.Background(), &pb.ListUsersRequest{})
    if err != nil {
        t.Fatalf("ListUsers: %v", err)
    }

    var users []*pb.User
    for {
        user, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            t.Fatalf("Recv: %v", err)
        }
        users = append(users, user)
    }

    if len(users) != 3 {
        t.Errorf("got %d users, want 3", len(users))
    }
}
Client Streaming
func TestBatchCreate_ClientStream(t *testing.T) {
    client := setupTest(t)
    stream, err := client.BatchCreateUsers(context.Background())
    if err != nil {
        t.Fatalf("BatchCreateUsers: %v", err)
    }

    for _, u := range testUsers {
        if err := stream.Send(u); err != nil {
            t.Fatalf("Send: %v", err)
        }
    }

    resp, err := stream.CloseAndRecv()
    if err != nil {
        t.Fatalf("CloseAndRecv: %v", err)
    }
    if resp.Created != int32(len(testUsers)) {
        t.Errorf("created = %d, want %d", resp.Created, len(testUsers))
    }
}

Testing Metadata

Verify that interceptors correctly read/write metadata:

func TestAuth_Metadata(t *testing.T) {
    client := setupTestWithInterceptors(t)

    // Without auth token → Unauthenticated
    _, err := client.GetUser(context.Background(), &pb.GetUserRequest{UserId: "1"})
    if st, _ := status.FromError(err); st.Code() != codes.Unauthenticated {
        t.Errorf("expected Unauthenticated without token, got %s", st.Code())
    }

    // With valid token → success
    md := metadata.Pairs("authorization", "Bearer valid-token")
    ctx := metadata.NewOutgoingContext(context.Background(), md)
    resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "1"})
    if err != nil {
        t.Fatalf("expected success with valid token: %v", err)
    }
    if resp.User == nil {
        t.Error("expected user in response")
    }
}

Testing Deadlines

func TestGetUser_DeadlineExceeded(t *testing.T) {
    client := setupTest(t) // server handler sleeps for 5s

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

    _, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "slow"})
    st, _ := status.FromError(err)
    if st.Code() != codes.DeadlineExceeded {
        t.Errorf("expected DeadlineExceeded, got %s", st.Code())
    }
}

Integration Test Patterns

For tests that hit real dependencies (database, external services), use build tags to separate them:

//go:build integration

func TestUserService_Integration(t *testing.T) {
    // Connect to real gRPC server
    conn, err := grpc.NewClient("localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        t.Fatalf("dial: %v", err)
    }
    defer conn.Close()

    client := pb.NewUserServiceClient(conn)

    // Test full roundtrip
    created, err := client.CreateUser(context.Background(), &pb.CreateUserRequest{
        Name:  "integration-test",
        Email: "[email protected]",
    })
    if err != nil {
        t.Fatalf("CreateUser: %v", err)
    }

    got, err := client.GetUser(context.Background(), &pb.GetUserRequest{
        UserId: created.User.Id,
    })
    if err != nil {
        t.Fatalf("GetUser: %v", err)
    }
    if got.User.Name != "integration-test" {
        t.Errorf("name = %q, want %q", got.User.Name, "integration-test")
    }
}

Run integration tests separately:

go test -tags=integration ./...

Requirements and compatibility

Written for: Claude Code, OpenAI Codex

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Claude Code frontmatter

The author says: Designed for Claude Code, Codex or similar harness, and for projects using Golang.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
samber
Publisher
Samuel Berthe (Community)
Distributed by
Samuel Berthe
Source
samber/cc-skills-golang · skills/golang-grpc · commit bb1d43d
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by samber or Samuel Berthe.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download golang-grpc.zip