Skip to content

Day 115 — Unary RPCs

Month 5 · Week 1 · ⬅ Day 114 · Day 116 ➡ · Journal index

🎯 Learning Objective

Stand up a gRPC server and client for a simple request/response (unary) method, and understand the lifecycle of one call from NewClient to Serve.

📚 Topics

  • grpc.NewServer, RegisterXxxServer, net.Listen, Serve
  • grpc.NewClient, insecure credentials, generated client interface, context first arg

📖 Reading / Sources

📝 Notes

  • A unary RPC is one request → one response: it looks like a normal method Get(ctx, *Req) (*Resp, error), but the bytes travel over HTTP/2 framed as protobuf → [[grpc]].
  • Server side: create a net.Listener, build a grpc.NewServer(), register your implementation with the generated RegisterUserServiceServer(s, impl), then s.Serve(lis) (blocks). Your impl type must embed UnimplementedUserServiceServer.
  • Every generated method takes context.Context as the first argument — the universal carry-all for deadlines, cancellation, and metadata. Honor it: check ctx.Err() in long handlers → [[context]].
  • Client side: grpc.NewClient(target, opts...) is the current API (it replaced the old grpc.Dial; NewClient does not connect eagerly — the connection forms lazily on first RPC). For local/dev use grpc.WithTransportCredentials(insecure.NewCredentials()).
  • Return typed errors with status.Error(codes.X, msg); returning a plain error is reported to the client as codes.Unknown. Covered in depth Day 118 → [[status-codes]].
  • Use the generated getters (req.GetId()) not field access — getters are nil-safe and return zero values, which matters because a nil message pointer is valid input.
  • Always defer conn.Close() on the client and wrap calls in a context.WithTimeout so a dead server doesn't hang you forever.

💻 Code Examples

// server.go — needs google.golang.org/grpc (shown as a snippet, not a runnable stdlib example)
type server struct {
    userv1.UnimplementedUserServiceServer // forward-compat: embed always
}

func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
    if req.GetId() == 0 {
        return nil, status.Error(codes.InvalidArgument, "id is required")
    }
    return &userv1.User{Id: req.GetId(), Name: "Ada"}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatal(err)
    }
    s := grpc.NewServer()
    userv1.RegisterUserServiceServer(s, &server{})
    log.Fatal(s.Serve(lis)) // blocks
}
// client.go
conn, err := grpc.NewClient("localhost:50051",
    grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

client := userv1.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

u, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})
if err != nil {
    log.Fatal(err)
}
fmt.Println(u.GetName())

gRPC needs third-party libs, so no stdlib example. The deadline/timeout mechanics every unary call uses are modeled with the stdlib in examples/month-05/deadline · Run: go run ./examples/month-05/deadline

🏋️ Exercises / Practice

Exercise Status Link
Status-code error model + HTTP mapping (used by every RPC return) exercises/month-05/week-1/statuscode

🐛 Mistakes Made

  • Used grpc.Dial(..., grpc.WithBlock()) out of habit; the docs now steer to grpc.NewClient, which is lazy and has no WithBlock. Switched.
  • Forgot to embed UnimplementedUserServiceServer and got a confusing "does not implement ... (missing method)" error.

❓ Open Questions

  • With NewClient being lazy, how do I fail fast at startup if the backend is down? (Looks like: trigger a real RPC or watch conn.GetState/WaitForStateChange.)

🧠 Active Recall (answer without looking)

  1. Q: What is the first parameter of every generated gRPC method, and why?
    A

context.Context. It carries the per-call deadline, cancellation signal, and metadata across the network boundary and down the handler call chain. Always pass it through, never context.Background() inside a handler. 2. Q: What does a handler that returns a plain errors.New("boom") look like to the client?

A

It arrives as a status with code codes.Unknown and the message "boom". To send a meaningful code, return status.Error(codes.X, msg) (or a status.Status.Err()).

🪶 Feynman Reflection

A unary RPC is a function call that happens to cross a network. The generated client gives you a struct whose methods marshal your request to protobuf, ship it over HTTP/2, and unmarshal the reply. The server is the mirror image. The only "new" ideas versus a local call are: a context rides along, and errors are codes, not arbitrary types.

🕳️ Knowledge Gaps

  • Connection pooling / load balancing inside a single ClientConn.
  • TLS credentials for production (vs. insecure) — Week 2.

✅ Summary

I can implement a unary RPC server, register it, serve it, and call it from a client with a deadline and proper status-code errors.

⏭️ Next Steps / Prep for Tomorrow

  • Day 116: server-streaming and client-streaming RPCs and how framing makes them work.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): unary rpcs (day 115)