08 - Building a gRPC Client
📋 Jump to TakeawaysYou have a running gRPC server. Now let's write a Go client that connects to it and calls methods. The client is generated from the same .proto file, so the types match exactly — no manual HTTP requests, no URL building, no JSON marshaling.
Project Structure
The client is a separate program. Create a cmd/client/ directory:
shortener/
├── go.mod
├── main.go ← server (lesson 07)
├── cmd/
│ └── client/
│ └── main.go ← client (this lesson)
├── proto/
│ └── link.proto
└── pb/
├── link.pb.go
└── link_grpc.pb.goYou'll run the server in one terminal and the client in another.
Connecting to the Server
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewLinkServiceClient(conn)grpc.NewClient creates a connection to the server. You might see grpc.Dial in older tutorials — that's deprecated. NewClient is the current API.
insecure.NewCredentials() disables TLS. gRPC requires you to explicitly choose a transport security option — without it, the connection fails. Use insecure for local development; in production you'd use real TLS credentials.
pb.NewLinkServiceClient(conn) returns a typed client with methods matching your service definition. This is the generated client code from link_grpc.pb.go — you just use it.
Calling RPCs
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Create a link
createResp, err := client.CreateLink(ctx, &pb.CreateLinkRequest{
Url: "https://example.com/some-long-article-url",
})
if err != nil {
log.Fatalf("CreateLink failed: %v", err)
}
fmt.Printf("created: %s -> %s\n", createResp.GetLink().GetShortCode(), createResp.GetLink().GetUrl())
// Get the link back
getResp, err := client.GetLink(ctx, &pb.GetLinkRequest{
ShortCode: createResp.GetLink().GetShortCode(),
})
if err != nil {
log.Fatalf("GetLink failed: %v", err)
}
fmt.Printf("fetched: %s (clicks: %d)\n", getResp.GetLink().GetUrl(), getResp.GetLink().GetClicks())It looks like calling a local function. That's the whole point of RPC — the network call, serialization, and deserialization happen behind the scenes.
Context and Timeouts
Always pass a context with a timeout. Without one, a broken server can hang your client forever.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.CreateLink(ctx, req)If the server doesn't respond within 5 seconds, the client gets a DeadlineExceeded error. The server also sees the cancellation through its own context and can stop work early.
Handling Errors
When a gRPC call fails, the error contains a status code and a message. Use the status package to extract them:
resp, err := client.GetLink(ctx, &pb.GetLinkRequest{ShortCode: "nonexistent"})
if err != nil {
st, ok := status.FromError(err)
if ok {
fmt.Printf("code: %s, message: %s\n", st.Code(), st.Message())
if st.Code() == codes.NotFound {
fmt.Println("link does not exist")
}
if st.Code() == codes.DeadlineExceeded {
fmt.Println("request timed out")
}
}
return
}status.FromError extracts the gRPC status from the error. You can check the code, read the message, and handle different error types differently. We'll go deeper on this in lesson 12.
Connection Management
A single grpc.ClientConn multiplexes many RPCs over one TCP connection (thanks to HTTP/2). You don't need a connection pool. Create one connection at startup and reuse it:
// Do this once at startup
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
defer conn.Close()
// Create clients from the same connection
linkClient := pb.NewLinkServiceClient(conn)The connection handles reconnection automatically. If the server goes down and comes back, the client reconnects without you doing anything.
Complete Client File
Here's the complete cmd/client/main.go:
package main
import (
"context"
"fmt"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"shortener/pb"
)
func main() {
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewLinkServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Create
created, err := client.CreateLink(ctx, &pb.CreateLinkRequest{Url: "https://example.com/some-long-article-url"})
if err != nil {
log.Fatal(err)
}
fmt.Printf("created: %+v\n", created.GetLink())
// Get
fetched, err := client.GetLink(ctx, &pb.GetLinkRequest{ShortCode: created.GetLink().GetShortCode()})
if err != nil {
log.Fatal(err)
}
fmt.Printf("fetched: %+v\n", fetched.GetLink())
// Delete
_, err = client.DeleteLink(ctx, &pb.DeleteLinkRequest{ShortCode: created.GetLink().GetShortCode()})
if err != nil {
log.Fatal(err)
}
fmt.Println("deleted")
// Try to get deleted link
_, err = client.GetLink(ctx, &pb.GetLinkRequest{ShortCode: created.GetLink().GetShortCode()})
if err != nil {
st, _ := status.FromError(err)
if st.Code() == codes.NotFound {
fmt.Println("confirmed: link is gone")
}
}
}Running the Client
Terminal 1 — start the server:
go run .
# gRPC server listening on :50051Terminal 2 — run the client:
go run ./cmd/clientExpected output:
created: id:1 url:"https://example.com/some-long-article-url" short_code:"k7m2px"
fetched: id:1 url:"https://example.com/some-long-article-url" short_code:"k7m2px"
deleted
confirmed: link is goneThe short code will be different each time (it's randomly generated).
Key Takeaways
grpc.NewClientcreates a connection (grpc.Dialis deprecated)insecure.NewCredentials()is required for local dev — gRPC won't connect without an explicit transport security choicepb.NewLinkServiceClient(conn)gives you a typed client from the generated code- Always pass a context with a timeout to prevent hanging calls
- Use
status.FromErrorto extract gRPC status codes from errors - One connection handles many concurrent RPCs — no connection pool needed
- The connection reconnects automatically if the server restarts