Go gRPC (Production)
Overview
gRPC provides strongly-typed RPC APIs backed by Protocol Buffers, with first-class streaming support and excellent performance for service-to-service communication. This skill focuses on production defaults: versioned protos, deadlines, error codes, interceptors, health checks, TLS, and testability.
Quick Start
- Define a versioned protobuf API
✅ Correct: versioned package
// proto/users/v1/users.proto syntax = "proto3";
package users.v1; option go_package = "example.com/myapp/gen/users/v1;usersv1";
service UsersService { rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc ListUsers(ListUsersRequest) returns (stream User); }
message GetUserRequest { string id = 1; } message GetUserResponse { User user = 1; } message ListUsersRequest { int32 page_size = 1; string page_token = 2; }
message User { string id = 1; string email = 2; string display_name = 3; }
❌ Wrong: unversioned package (hard to evolve)
package users;
- Generate Go code
Install generators:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Generate:
protoc -I proto
--go_out=./gen --go_opt=paths=source_relative
--go-grpc_out=./gen --go-grpc_opt=paths=source_relative
proto/users/v1/users.proto
- Implement server with deadlines and status codes
✅ Correct: validate + map errors to gRPC codes
package usersvc
import ( "context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
usersv1 "example.com/myapp/gen/users/v1"
)
type Service struct { usersv1.UnimplementedUsersServiceServer Repo Repo }
type Repo interface { GetUser(ctx context.Context, id string) (User, error) }
type User struct { ID, Email, DisplayName string }
func (s *Service) GetUser(ctx context.Context, req *usersv1.GetUserRequest) (*usersv1.GetUserResponse, error) { if req.GetId() == "" { return nil, status.Error(codes.InvalidArgument, "id is required") }
u, err := s.Repo.GetUser(ctx, req.GetId())
if err != nil {
if err == ErrNotFound {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "internal error")
}
return &usersv1.GetUserResponse{
User: &usersv1.User{
Id: u.ID,
Email: u.Email,
DisplayName: u.DisplayName,
},
}, nil
}
❌ Wrong: return raw errors (clients lose code semantics)
return nil, errors.New("user not found")
Core Concepts
Deadlines and cancellation
Make every call bounded; enforce server-side timeouts for expensive handlers.
✅ Correct: require deadline
if _, ok := ctx.Deadline(); !ok { return nil, status.Error(codes.InvalidArgument, "deadline required") }
Metadata
Use metadata for auth/session correlation, not for primary request data.
✅ Correct: read auth token from metadata
md, _ := metadata.FromIncomingContext(ctx) auth := "" if vals := md.Get("authorization"); len(vals) > 0 { auth = vals[0] }
Interceptors (Middleware)
Use interceptors for cross-cutting concerns: auth, logging, metrics, tracing, request IDs.
✅ Correct: unary interceptor with request ID
func unaryRequestID() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { id := uuid.NewString() ctx = context.WithValue(ctx, requestIDKey{}, id) resp, err := handler(ctx, req) return resp, err } }
Streaming patterns
Server streaming (paginate or stream results)
✅ Correct: stop on ctx.Done()
func (s *Service) ListUsers(req *usersv1.ListUsersRequest, stream usersv1.UsersService_ListUsersServer) error { users, err := s.Repo.ListUsers(stream.Context(), int(req.GetPageSize())) if err != nil { return status.Error(codes.Internal, "internal error") }
for _, u := range users {
select {
case <-stream.Context().Done():
return stream.Context().Err()
default:
}
if err := stream.Send(&usersv1.User{
Id: u.ID,
Email: u.Email,
DisplayName: u.DisplayName,
}); err != nil {
return err
}
}
return nil
}
Unary vs streaming decision
-
Use unary for single request/response and simple retries.
-
Use server streaming for large result sets or continuous updates.
-
Use client streaming for bulk uploads with one final response.
-
Use bidirectional streaming for interactive protocols.
Production Hardening
Health checks and reflection
Add health service; enable reflection only in non-production environments.
✅ Correct: health + conditional reflection
hs := health.NewServer() grpc_health_v1.RegisterHealthServer(s, hs)
if env != "production" { reflection.Register(s) }
Graceful shutdown
Prefer GracefulStop with a deadline.
✅ Correct: graceful stop
stopped := make(chan struct{}) go func() { grpcServer.GracefulStop() close(stopped) }()
select { case <-stopped: case <-time.After(10 * time.Second): grpcServer.Stop() }
TLS
Use TLS (or mTLS) in production; avoid insecure credentials outside local dev.
✅ Correct: server TLS
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key") if err != nil { return err }
grpcServer := grpc.NewServer(grpc.Creds(creds))
Testing (bufconn)
Test gRPC handlers without opening real sockets using bufconn .
✅ Correct: in-memory gRPC test server
const bufSize = 1024 * 1024
lis := bufconn.Listen(bufSize) srv := grpc.NewServer() usersv1.RegisterUsersServiceServer(srv, &Service{Repo: repo})
go func() { _ = srv.Serve(lis) }()
ctx := context.Background() conn, err := grpc.DialContext( ctx, "bufnet", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { t.Fatal(err) } defer conn.Close()
client := usersv1.NewUsersServiceClient(conn) resp, err := client.GetUser(ctx, &usersv1.GetUserRequest{Id: "1"}) _ = resp _ = err
Anti-Patterns
Ignore deadlines: unbounded handlers cause tail latency and resource exhaustion.
Return string errors: map domain errors to codes.* with status.Error or status.Errorf .
Stream without backpressure: stop on ctx.Done() and handle Send errors.
Expose reflection in production: treat reflection as a discovery surface.
Troubleshooting
Symptom: clients see UNKNOWN errors
Actions:
-
Return status.Error(codes.X, "...") instead of raw errors.
-
Wrap domain errors into typed errors, then map to gRPC codes.
Symptom: slow/hanging requests
Actions:
-
Require deadlines and propagate ctx to downstream calls.
-
Add server-side timeouts and bounded concurrency in repositories.
Symptom: flaky streaming
Actions:
-
Stop streaming on ctx.Done() and handle stream.Send errors.
-
Avoid buffering entire result sets before sending.
Resources
-
gRPC Go: https://github.com/grpc/grpc-go
-
Protobuf Go: https://pkg.go.dev/google.golang.org/protobuf
-
gRPC error codes: https://grpc.io/docs/guides/error/