Implementing Unary RPCs
1. Creating Server Handler
Example: Unary handler
func (s *server) GetUser(ctx context.Context, r *userv1.GetUserRequest) (*userv1.User, error) {
if r.GetId() == "" {
return nil, status.Error(codes.InvalidArgument, "id is required")
}
u, err := s.repo.Find(ctx, r.GetId())
if errors.Is(err, repo.ErrNotFound) {
return nil, status.Error(codes.NotFound, "user not found")
}
if err != nil { return nil, status.Errorf(codes.Internal, "db: %v", err) }
return u, nil
}
| Rule | Detail |
|---|---|
| Signature | (ctx, *Req) (*Res, error) |
| Always return status errors | Use status.Error / Errorf |
Honor ctx | Pass to downstream calls |
2. Receiving Request Context
| From ctx | API |
|---|---|
| Deadline | ctx.Deadline() |
| Cancellation | ctx.Done() |
| Metadata | metadata.FromIncomingContext(ctx) |
| Peer info | peer.FromContext(ctx) |
3. Accessing Request Fields
| API | Detail |
|---|---|
r.GetX() | Nil-safe getter — preferred |
r.X | Direct access — panics on nil request |
| Presence (optional) | r.X != nil for messages / wrappers |
4. Returning Response
| Pattern | Detail |
|---|---|
| Success | return &Res{...}, nil |
| Failure | return nil, status.Error(code, msg) |
| Partial response | Avoid — return either or use streaming |
5. Returning Errors
Example: Rich error details
st := status.New(codes.InvalidArgument, "bad email")
st, _ = st.WithDetails(&errdetails.BadRequest_FieldViolation{
Field: "email", Description: "not RFC 5322",
})
return nil, st.Err()
| Common Code | Use |
|---|---|
| InvalidArgument | Bad input |
| NotFound | Missing resource |
| AlreadyExists | Duplicate creation |
| PermissionDenied | AuthZ failure |
| Unauthenticated | No/invalid creds |
| Internal | Unexpected server error |
6. Validating Request Data
| Approach | Detail |
|---|---|
| Inline checks | Simple, explicit; clutters handler |
| protovalidate | CEL rules in .proto |
| Validation interceptor | Centralized — calls Validate() automatically |
7. Creating Client Call
Example: Client unary call
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
u, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: "u1"})
if err != nil {
st, _ := status.FromError(err)
log.Printf("%s: %s", st.Code(), st.Message())
return
}
8. Setting Call Options
| Option | Use |
|---|---|
grpc.WaitForReady(true) | Queue until conn ready |
grpc.MaxCallRecvMsgSize(n) | Per-call recv limit |
grpc.UseCompressor("gzip") | Enable per-call compression |
grpc.Header(&md) | Capture response headers |
grpc.Trailer(&md) | Capture trailers |
9. Handling Client Response
| Pattern | Detail |
|---|---|
Always check err | Don't read response on error |
Use status.FromError | Extract code + message + details |
| Idempotent retries | Only on Unavailable/DeadlineExceeded for safe ops |
10. Using Context Cancellation
Example: Cancel mid-flight
ctx, cancel := context.WithCancel(context.Background())
go func() { time.Sleep(100 * time.Millisecond); cancel() }()
_, err := client.SlowOp(ctx, req) // err has codes.Canceled
| Behavior | Detail |
|---|---|
| Server sees | ctx.Done() closes, ctx.Err() == context.Canceled |
| Client receives | codes.Canceled |