
Golang Grpc
- 33.6k installs
- 2.8k repo stars
- Updated July 27, 2026
- samber/cc-skills-golang
golang-grpc is a Go backend development skill that teaches gRPC error handling, proto message design, and server lifecycle management.
About
Go gRPC skill covering error handling with status.Errorf, proto message design patterns, directory organization, and graceful server shutdown. Teaches developers to implement properly typed gRPC handlers, use wrapper messages for backward compatibility, and manage service lifecycle correctly.
- Error handling with status.Errorf and specific gRPC codes
- Proto wrapper messages for backward-compatible API evolution
- Graceful server shutdown with timeout fallback
Golang Grpc by the numbers
- 33,606 all-time installs (skills.sh)
- +450 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #23 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
golang-grpc capabilities & compatibility
- Capabilities
- api development · error handling · protobuf design
- Use cases
- api development
What golang-grpc says it does
Tests that gRPC handlers return status.Errorf with specific codes, not raw errors
Tests that proto RPCs use Request/Response wrapper messages, not bare types
npx skills add https://github.com/samber/cc-skills-golang --skill golang-grpcAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33.6k |
|---|---|
| repo stars | ★ 2.8k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | samber/cc-skills-golang ↗ |
How do you return gRPC status codes in Go?
Developers building gRPC services in Go need correct error propagation, proto design patterns, and production-ready shutdown handling.
Who is it for?
Microservices developers building Go gRPC services
Skip if: REST-only Go APIs or teams not using google.golang.org/grpc and protobuf-generated services.
When should I use this skill?
Building gRPC services in Go, designing proto schemas, implementing error handling
What you get
gRPC handlers using status.Errorf, correct codes.NotFound mappings, and protobuf services following production error conventions.
- status-coded gRPC handlers
- protobuf service implementations
Files
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. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform.
Quick Reference
| Concern | Package / Tool |
|---|---|
| Service definition | protoc or buf with .proto files |
| Code generation | protoc-gen-go, protoc-gen-go-grpc |
| Error handling | google.golang.org/grpc/status with codes |
| Rich error details | google.golang.org/genproto/googleapis/rpc/errdetails |
| Interceptors | grpc.ChainUnaryInterceptor, grpc.ChainStreamInterceptor |
| Middleware ecosystem | github.com/grpc-ecosystem/go-grpc-middleware |
| Testing | google.golang.org/grpc/test/bufconn |
| TLS / mTLS | google.golang.org/grpc/credentials |
| Health checks | google.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 toStop()— 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_robinwith headless Kubernetes services viadns:///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.
| Code | When to Use |
|---|---|
InvalidArgument | Malformed input (missing field, bad format) |
NotFound | Entity does not exist |
AlreadyExists | Create failed, entity exists |
PermissionDenied | Caller lacks permission |
Unauthenticated | Missing or invalid token |
FailedPrecondition | System not in required state |
ResourceExhausted | Rate limit or quota exceeded |
Unavailable | Transient issue, safe to retry |
Internal | Unexpected bug |
DeadlineExceeded | Timeout |
// ✗ 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
| Pattern | Use Case |
|---|---|
| Server streaming | Server sends a sequence (log tailing, result sets) |
| Client streaming | Client sends a sequence, server responds once (file upload, batch) |
| Bidirectional | Both 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.PerRPCCredentialsand validate tokens in an auth interceptor - Reflection SHOULD be disabled in production to prevent API discovery
Performance
| Setting | Purpose | Typical Value |
|---|---|---|
keepalive.ServerParameters.Time | Ping interval for idle connections | 30s |
keepalive.ServerParameters.Timeout | Ping ack timeout | 10s |
grpc.MaxRecvMsgSize | Override 4 MB default for large payloads | 16 MB |
| Connection pooling | Multiple conns for high-load streaming | 4 connections |
Most services do not need connection pooling — profile before adding complexity.
Common Mistakes
| Mistake | Fix |
|---|---|
Returning raw error | Becomes codes.Unknown — client can't decide whether to retry. Use status.Errorf with a specific code |
| No deadline on client calls | Slow upstream hangs indefinitely. Always context.WithTimeout |
| New connection per request | Wastes TCP/TLS handshakes. Create once, reuse — HTTP/2 multiplexes RPCs |
| Reflection enabled in production | Lets attackers enumerate every method. Enable only in dev/staging |
codes.Internal for all errors | Wrong codes break client retry logic. Unavailable triggers retry; InvalidArgument does not |
| Bare types as RPC arguments | Can't add fields to string. Wrapper messages allow backwards-compatible evolution |
| Missing health check service | Kubernetes can't determine readiness, kills pods during deployments |
| Ignoring context cancellation | Long operations continue after caller gave up. Check ctx.Err() |
Cross-References
- → See
samber/cc-skills-golang@golang-contextskill for deadline and cancellation patterns - → See
samber/cc-skills-golang@golang-error-handlingskill for gRPC error to Go error mapping - → See
samber/cc-skills-golang@golang-observabilityskill for gRPC interceptors (logging, tracing, metrics) - → See
samber/cc-skills-golang@golang-testingskill for gRPC testing with bufconn
[
{
"id": 1,
"name": "raw-error-vs-status-error",
"description": "Tests that gRPC handlers return status.Errorf with specific codes, not raw errors",
"prompt": "Write a Go gRPC handler for GetUser that looks up a user from a repository. If the user is not found, return an appropriate error. If the repository returns an unexpected error, return that too.",
"trap": "Without the skill, the model may return fmt.Errorf or raw errors, which become codes.Unknown. The skill requires status.Errorf with specific codes.",
"assertions": [
{"id": "1.1", "text": "Uses status.Errorf (or status.Error) for the not-found case with codes.NotFound"},
{"id": "1.2", "text": "Uses status.Errorf with codes.Internal for unexpected errors"},
{"id": "1.3", "text": "Does NOT return a raw fmt.Errorf or errors.New as the gRPC error"},
{"id": "1.4", "text": "Imports google.golang.org/grpc/status and google.golang.org/grpc/codes"},
{"id": "1.5", "text": "Does NOT leak internal error details in the user-facing gRPC message for Internal errors"}
]
},
{
"id": 2,
"name": "wrapper-messages-not-bare-types",
"description": "Tests that proto RPCs use Request/Response wrapper messages, not bare types",
"prompt": "Write a proto3 service definition for a product catalog with RPCs: GetProduct (takes product ID, returns product), DeleteProduct (takes product ID, returns nothing), SearchProducts (takes search query string, returns list of products).",
"trap": "Without the skill, the model may use bare types like string or google.protobuf.Empty. The skill requires Request/Response wrappers for backward-compatible evolution.",
"assertions": [
{"id": "2.1", "text": "Uses GetProductRequest/GetProductResponse wrapper messages, not bare string or Product"},
{"id": "2.2", "text": "Uses DeleteProductRequest/DeleteProductResponse wrapper messages, not bare string or google.protobuf.Empty"},
{"id": "2.3", "text": "Uses SearchProductsRequest/SearchProductsResponse wrapper messages"},
{"id": "2.4", "text": "Each wrapper message has properly named fields (not just a single unnamed field)"},
{"id": "2.5", "text": "Includes go_package option in the proto file"}
]
},
{
"id": 3,
"name": "proto-directory-organization",
"description": "Tests proper proto file organization by domain with versioned directories",
"prompt": "I'm building a microservice with user management and order management. How should I organize my proto files? Show the directory layout and an example buf.gen.yaml.",
"trap": "Without the skill, the model may put all protos in a flat directory or skip versioning. The skill requires domain-based organization with v1 directories.",
"assertions": [
{"id": "3.1", "text": "Organizes proto files by domain (user/, order/ or similar domain grouping)"},
{"id": "3.2", "text": "Includes version directories (v1/ under each domain)"},
{"id": "3.3", "text": "Separates message definitions from service definitions (e.g. user.proto vs user_service.proto)"},
{"id": "3.4", "text": "Includes a shared/ or common/ directory for shared messages"},
{"id": "3.5", "text": "Shows buf.gen.yaml with go and go-grpc plugins configured"}
]
},
{
"id": 4,
"name": "graceful-stop-with-timeout-fallback",
"description": "Tests proper gRPC server shutdown: GracefulStop with timeout fallback to Stop",
"prompt": "Write Go code for a gRPC server that handles shutdown signals properly. The server should try to drain in-flight requests before stopping.",
"trap": "Without the skill, the model may only call srv.Stop() or only srv.GracefulStop() without a timeout fallback. The skill requires GracefulStop with a timeout fallback to Stop.",
"assertions": [
{"id": "4.1", "text": "Calls srv.GracefulStop() first to drain in-flight RPCs"},
{"id": "4.2", "text": "Has a timeout mechanism that falls back to srv.Stop() if GracefulStop takes too long"},
{"id": "4.3", "text": "Uses a select statement or timer for the timeout"},
{"id": "4.4", "text": "Listens for OS signals (SIGINT, SIGTERM or os.Interrupt)"},
{"id": "4.5", "text": "The timeout is reasonable (5-30 seconds)"}
]
},
{
"id": 5,
"name": "health-check-service-registration",
"description": "Tests that gRPC servers register the health check service for Kubernetes readiness probes",
"prompt": "Write a production-ready gRPC server setup in Go. Register a UserService and make it deployable to Kubernetes.",
"trap": "Without the skill, the model may not register the health check service. The skill says health check is required for Kubernetes probes.",
"assertions": [
{"id": "5.1", "text": "Registers grpc_health_v1.RegisterHealthServer (or equivalent health check service)"},
{"id": "5.2", "text": "Uses health.NewServer() to create the health service"},
{"id": "5.3", "text": "Registers interceptors using grpc.ChainUnaryInterceptor"},
{"id": "5.4", "text": "Does NOT enable reflection (or explicitly disables it for production)"},
{"id": "5.5", "text": "Includes graceful shutdown handling"}
]
},
{
"id": 6,
"name": "client-connection-reuse-and-deadlines",
"description": "Tests that gRPC clients reuse connections and set deadlines on every call",
"prompt": "Write a Go gRPC client that calls a UserService. Create the client and make a GetUser call.",
"trap": "Without the skill, the model may create a new connection per request or not set a deadline. The skill requires connection reuse and context.WithTimeout on every call.",
"assertions": [
{"id": "6.1", "text": "Creates the connection once and reuses it (not creating a new connection per call)"},
{"id": "6.2", "text": "Uses context.WithTimeout (or context.WithDeadline) for the RPC call"},
{"id": "6.3", "text": "Uses grpc.NewClient (not the deprecated grpc.Dial)"},
{"id": "6.4", "text": "Includes transport credentials (TLS or explicitly insecure for dev)"},
{"id": "6.5", "text": "Properly defers cancel() from context.WithTimeout"}
]
},
{
"id": 7,
"name": "bufconn-testing-pattern",
"description": "Tests that gRPC tests use bufconn for in-memory connections, not real network",
"prompt": "Write a test for a Go gRPC UserService that verifies GetUser returns the correct user and that requesting a non-existent user returns the proper error code.",
"trap": "Without the skill, the model may start a real TCP server for testing. The skill requires bufconn for in-memory connections.",
"assertions": [
{"id": "7.1", "text": "Uses bufconn.Listen for an in-memory listener"},
{"id": "7.2", "text": "Uses grpc.WithContextDialer with the bufconn dialer"},
{"id": "7.3", "text": "Verifies the gRPC status code for the not-found case using status.FromError"},
{"id": "7.4", "text": "Checks that the error code is codes.NotFound specifically"},
{"id": "7.5", "text": "Uses t.Cleanup or defer for cleaning up the server and connection"}
]
},
{
"id": 8,
"name": "error-code-selection-judgment",
"description": "Tests correct gRPC error code selection across different scenarios",
"prompt": "Write a Go gRPC handler for CreateOrder that validates the request has a non-empty user_id and at least one item, checks the user exists, checks inventory availability, and creates the order. Return appropriate gRPC errors for each failure case.",
"trap": "Without the skill, the model may use codes.Internal for everything or pick wrong codes. The skill has a specific mapping table.",
"assertions": [
{"id": "8.1", "text": "Uses codes.InvalidArgument for missing user_id or empty items list"},
{"id": "8.2", "text": "Uses codes.NotFound for user not found"},
{"id": "8.3", "text": "Uses codes.FailedPrecondition for insufficient inventory (system not in required state)"},
{"id": "8.4", "text": "Uses codes.Internal only for truly unexpected errors"},
{"id": "8.5", "text": "Does NOT use codes.Unknown for any handled error case"}
]
},
{
"id": 9,
"name": "unimplemented-server-embedding",
"description": "Tests that gRPC server implementations embed UnimplementedXxxServer, not UnsafeXxxServer",
"prompt": "Write a Go implementation of a gRPC server for an OrderService with CreateOrder and GetOrder RPCs. Show the struct definition and method signatures.",
"trap": "Without the skill, the model may embed UnsafeXxxServer or not embed any base server. The skill requires UnimplementedXxxServer for forward compatibility.",
"assertions": [
{"id": "9.1", "text": "Embeds UnimplementedOrderServiceServer in the server struct"},
{"id": "9.2", "text": "Does NOT embed UnsafeOrderServiceServer"},
{"id": "9.3", "text": "Implements the service methods with correct signatures (context, request pointer, response pointer and error return)"},
{"id": "9.4", "text": "Uses status.Errorf for error returns in the handler methods"},
{"id": "9.5", "text": "Registers the server with pb.RegisterOrderServiceServer"}
]
},
{
"id": 10,
"name": "client-load-balancing-and-retry",
"description": "Tests knowledge of gRPC client-side load balancing and retry configuration",
"prompt": "I'm deploying a Go gRPC client in Kubernetes. The server has multiple replicas behind a headless service. How do I configure the client to distribute requests across all replicas and retry on transient failures?",
"trap": "Without the skill, the model may suggest a separate load balancer or basic round-robin without the dns:/// scheme. The skill shows the specific configuration.",
"assertions": [
{"id": "10.1", "text": "Uses the dns:/// scheme for service discovery with headless Kubernetes services"},
{"id": "10.2", "text": "Configures round_robin load balancing policy via service config"},
{"id": "10.3", "text": "Configures retry policy in the service config with retryableStatusCodes containing UNAVAILABLE"},
{"id": "10.4", "text": "Sets maxAttempts, initialBackoff, maxBackoff in the retry policy"},
{"id": "10.5", "text": "Does NOT suggest creating a new connection per request for load distribution"}
]
},
{
"id": 11,
"name": "streaming-over-large-messages",
"description": "Tests preference for streaming over large single messages",
"prompt": "I need to send a result set of 50,000 records from a gRPC server to a client. Each record is about 1KB. What's the best approach?",
"trap": "Without the skill, the model may suggest increasing MaxRecvMsgSize and sending all records in one response. The skill recommends streaming to avoid per-message size limits and memory pressure.",
"assertions": [
{"id": "11.1", "text": "Recommends server streaming RPC to send records incrementally"},
{"id": "11.2", "text": "Explains why a single large message is problematic (size limits, memory pressure)"},
{"id": "11.3", "text": "Shows the server streaming pattern with stream.Send in a loop"},
{"id": "11.4", "text": "Client reads with stream.Recv in a loop, checking for io.EOF"},
{"id": "11.5", "text": "Does NOT just increase MaxRecvMsgSize as the primary solution"}
]
}
]
Protobuf & Code Generation Reference
Directory Layout
Organize proto files by domain with versioned directories. Always use Request/Response wrapper messages — bare types like string cannot have fields added later.
proto/
├── user/v1/
│ ├── user.proto # Messages
│ └── user_service.proto # Service RPCs
├── order/v1/
│ ├── order.proto
│ └── order_service.proto
└── shared/v1/
└── common.proto # Pagination, timestamps, shared enumsProto File Conventions
syntax = "proto3";
package mycompany.user.v1;
option go_package = "github.com/mycompany/myservice/gen/user/v1;userv1";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}
message GetUserRequest {
string user_id = 1;
}
message GetUserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}go_package conventions
- Format:
"import/path;alias"— the alias becomes the Go package name - Convention: lowercase version suffix (e.g.
userv1,orderv1) - Generate into a
gen/directory to keep generated code separate from hand-written code
Code Generation with protoc
# Basic generation
protoc --go_out=gen --go_opt=paths=source_relative \
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
proto/user/v1/*.proto
# With validation (using buf-validate)
protoc --go_out=gen --go_opt=paths=source_relative \
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
--validate_out="lang=go:gen" \
proto/user/v1/*.proto
# Include external imports
protoc -I proto -I third_party \
--go_out=gen --go_opt=paths=source_relative \
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
proto/user/v1/*.protoCommon protoc flags
| Flag | Purpose |
|---|---|
--go_out=DIR | Output directory for message types |
--go-grpc_out=DIR | Output directory for service stubs |
--go_opt=paths=source_relative | Place output relative to proto source |
-I DIR | Add import path for proto dependencies |
--descriptor_set_out=FILE | Emit binary descriptor (for reflection) |
Code Generation with buf
buf is the recommended modern alternative to raw protoc. It manages dependencies, lints protos, and generates code from a single config.
buf.gen.yaml
version: v2
plugins:
- remote: buf.build/protocolbuffers/go
out: gen
opt: paths=source_relative
- remote: buf.build/grpc/go
out: gen
opt: paths=source_relativebuf.yaml
version: v2
modules:
- path: proto
lint:
use:
- STANDARD
breaking:
use:
- FILECommon buf commands
buf generate # Generate code from buf.gen.yaml
buf lint # Lint proto files
buf breaking --against '.git#branch=main' # Check backward compatibility
buf dep update # Update dependencies
buf build # Validate proto files compileGenerated Code Patterns
After generation, import and use the generated code:
import (
userv1 "github.com/mycompany/myservice/gen/user/v1"
)
// Server: implement the interface
type userServer struct {
userv1.UnimplementedUserServiceServer
}
// Client: use the generated client
client := userv1.NewUserServiceClient(conn)
resp, err := client.GetUser(ctx, &userv1.GetUserRequest{UserId: "123"})Always embed Unimplemented*Server (not Unsafe*Server) — it provides forward compatibility when new RPCs are added to the proto definition.
gRPC Testing Reference
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: "test@example.com",
})
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 ./...Related skills
How it compares
Use golang-grpc for RPC error semantics; use golang-observability to trace gRPC calls across services in production.
FAQ
Why does golang-grpc reject raw fmt.Errorf in handlers?
golang-grpc rejects raw fmt.Errorf because gRPC Go converts unwrapped errors to codes.Unknown. The skill requires status.Errorf or status.Error with explicit codes such as codes.NotFound so clients receive meaningful RPC status responses.
Which gRPC code should missing users use?
golang-grpc requires codes.NotFound via status.Errorf when a GetUser repository lookup finds no record. Unexpected repository failures must map to appropriate gRPC codes rather than leaking raw internal errors.
Is Golang Grpc safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.