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,Servegrpc.NewClient,insecurecredentials, generated client interface,contextfirst 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 agrpc.NewServer(), register your implementation with the generatedRegisterUserServiceServer(s, impl), thens.Serve(lis)(blocks). Your impl type must embedUnimplementedUserServiceServer. - Every generated method takes
context.Contextas the first argument — the universal carry-all for deadlines, cancellation, and metadata. Honor it: checkctx.Err()in long handlers → [[context]]. - Client side:
grpc.NewClient(target, opts...)is the current API (it replaced the oldgrpc.Dial;NewClientdoes not connect eagerly — the connection forms lazily on first RPC). For local/dev usegrpc.WithTransportCredentials(insecure.NewCredentials()). - Return typed errors with
status.Error(codes.X, msg); returning a plainerroris reported to the client ascodes.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 acontext.WithTimeoutso 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 togrpc.NewClient, which is lazy and has noWithBlock. Switched. - Forgot to embed
UnimplementedUserServiceServerand got a confusing "does not implement ... (missing method)" error.
❓ Open Questions¶
- With
NewClientbeing lazy, how do I fail fast at startup if the backend is down? (Looks like: trigger a real RPC or watchconn.GetState/WaitForStateChange.)
🧠 Active Recall (answer without looking)¶
- 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)