rgoussu@goussu: ~/library/go/protocols
~/library/go/protocols cat grpc.md

gRPC in Go

# grpc-go and the buf toolchain, streaming modes, interceptors, connect-go and grpc-gateway, and load-balancing realities in Kubernetes.

Conceptsaved 2026-08-09 #go#protocols#grpc#protobuf#microservices

Overview

Go is gRPC's home turf: grpc-go is a reference implementation maintained alongside the protocol itself, and the protobuf toolchain generates idiomatic Go stubs. The interesting decisions are around the edges — buf vs raw protoc for schema management, connect-go as the HTTP-compatible rethink, grpc-gateway for REST fallback, and the perennial gotcha of load balancing long-lived HTTP/2 connections in Kubernetes.

Key points

  • grpc-go (google.golang.org/grpc): the reference implementation — HTTP/2 transport, generated client stubs and server interfaces, context.Context threading cancellation and deadlines through every call.
  • Toolchain: classic is protoc + protoc-gen-go + protoc-gen-go-grpc; the modern default is bufbuf generate with remote plugins (no local protoc install), buf lint for style, buf breaking for breaking-change detection against the previous version, and the Buf Schema Registry for sharing contracts.
  • Four streaming modes: unary, server-streaming, client-streaming, bidirectional — streams surface as generated Send/Recv methods; a goroutine per direction is the natural consumption pattern.
  • Deadlines & metadata: deadlines propagate via context.WithTimeout and cross the wire (grpc-timeout header); metadata (metadata.NewOutgoingContext) carries auth tokens and tracing headers.
  • Interceptors: the middleware story — unary and stream variants, chained with grpc.ChainUnaryInterceptor; otelgrpc (now via stats.Handler rather than interceptors) for tracing, go-grpc-middleware for retry/auth/recovery stock parts.
  • connect-go (Connect RPC, now a CNCF project): same .proto contracts, but plain HTTP semantics — handlers mount on net/http, one endpoint speaks gRPC, gRPC-Web and Connect's own JSON/HTTP protocol, so browsers and curl work without a proxy.
  • grpc-gateway: generates a reverse proxy from google.api.http annotations, exposing a REST/JSON API that transcodes to your gRPC service — the older answer to the same "browsers can't speak gRPC" problem.
  • Operational endpoints: register the standard health service (grpc_health_v1, used by Kubernetes gRPC probes) and reflection (reflection.Register) so grpcurl/grpcui work against the service.

Details

Load balancing in Kubernetes

The classic trap: gRPC holds one HTTP/2 connection, and a ClusterIP Service balances connections, not requests — so all traffic pins to one pod. Options, in rough order of adoption:

Approach Mechanism Notes
Client-side LB + headless Service dns:///svc.ns.svc.cluster.local, round_robin config No infra needed; DNS re-resolution lags pod churn
Service mesh / L7 proxy Istio/Linkerd/Envoy balance per-request Zero client changes; operational cost of the mesh
Proxyless xDS grpc-go's xDS support talks to the mesh control plane directly Newer; mesh features without the sidecar

MaxConnectionAge on the server keeps connections cycling so client-side balancing rediscovers pods.

connect-go vs grpc-go vs grpc-gateway

connect-go keeps the protobuf contract but drops grpc-go's bespoke HTTP/2 server for plain net/http, which buys: standard middleware, HTTP/1.1 compatibility, browser support without Envoy, and easily curl-able JSON. Interop is real — connect servers accept gRPC clients and vice versa. grpc-gateway remains the choice when you must publish a genuine REST API (OpenAPI included) from proto annotations rather than a third protocol. Staying on grpc-go proper makes sense when you need its full feature surface (xDS, advanced LB policies) or ecosystem parity with other reference implementations.

Examples

// Server with chained interceptors, health and reflection.
s := grpc.NewServer(
    grpc.ChainUnaryInterceptor(recoveryInterceptor, authInterceptor),
    grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
pb.RegisterOrderServiceServer(s, &orderServer{})
grpc_health_v1.RegisterHealthServer(s, health.NewServer())
reflection.Register(s)

// Client call with a deadline; client-side round robin over a headless service.
conn, _ := grpc.NewClient("dns:///orders.default.svc.cluster.local:50051",
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`))
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
resp, err := pb.NewOrderServiceClient(conn).GetOrder(ctx, &pb.GetOrderRequest{Id: id})
buf lint && buf breaking --against '.git#branch=main' && buf generate

Related