Explain gRPC in Go: unary vs streaming RPCs, interceptors, deadlines, status codes and error details.
RPC kinds. gRPC has four:
- Unary: one request, one response.
- Server streaming: one request, then
stream.Sendmany times. - Client streaming:
Recvmany times, thenSendAndClose. - Bidirectional: both sides send and receive independently.
A stream is one HTTP/2 stream. Send is not safe to call from several goroutines at once, and neither is Recv. One sending goroutine and one receiving goroutine is fine.
Interceptors are middleware: grpc.ChainUnaryInterceptor and grpc.ChainStreamInterceptor on the server, and WithChainUnaryInterceptor on the client. Use them for auth, logging, metrics and panic recovery.
Deadlines come from the client's ctx. They travel over the wire as grpc-timeout, and the server sees them as ctx.Done(). Always set one, because the default is no deadline at all.
func recoverUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo,
h grpc.UnaryHandler) (resp any, err error) {
defer func() {
if r := recover(); r != nil {
err = status.Errorf(codes.Internal, "panic in %s", info.FullMethod)
}
}()
return h(ctx, req)
}
func (s *server) GetUser(ctx context.Context, r *pb.GetUserRequest) (*pb.User, error) {
if r.GetId() == "" {
st := status.New(codes.InvalidArgument, "id is required")
st, _ = st.WithDetails(&errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{
{Field: "id", Description: "must not be empty"}}})
return nil, st.Err()
}
u, err := s.db.Find(ctx, r.GetId())
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "user not found")
}
return u, err
}
// client
ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
defer cancel()
_, err := client.GetUser(ctx, &pb.GetUserRequest{})
switch status.Code(err) {
case codes.DeadlineExceeded, codes.Unavailable: // candidates for retry
case codes.InvalidArgument: // do not retry
}
What does this print? fmt.Println(status.Code(nil), status.Code(errors.New("x"))) prints OK Unknown. If a handler returns a plain Go error, the client sees Unknown. Map errors to proper codes at the service boundary.
Other gotchas:
- Use
grpc.NewClient(Dialis deprecated). It connects lazily. - Reuse one
ClientConnper target. - Return
ctx.Err()promptly when the client cancels. - Never wrap a status error with
fmt.Errorf("%w")and expectstatus.Codeto find the code on grpc-go versions older than 1.55. Older versions do not unwrap.
More on Observability, Debugging & Production Operations
- Q566How do you instrument a Go service with Prometheus metrics (counters, gauges, histograms)? What are the label cardinality pitfalls?
- Q567How do you add distributed tracing with OpenTelemetry in Go? How does the trace context flow through context.Context and HTTP/gRPC?
- Q569How do you keep protobuf messages backward and forward compatible? Why must you never reuse field numbers?
- Q570How do you implement health checks (liveness vs readiness) for a Go service running in Kubernetes, and how do they interact with graceful shutdown?
- Q571How do you build a minimal, secure Docker image for a Go service (multi-stage build, scratch/distroless, CGO_ENABLED=0, CA certs, time zone data)?
- Q572How do you manage configuration in Go (env vars, files, flags)? How do you validate it and reload it safely at run time?