08 - Building a gRPC Client

📋 Jump to Takeaways

You 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.go

You'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 :50051

Terminal 2 — run the client:

go run ./cmd/client

Expected 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 gone

The short code will be different each time (it's randomly generated).

Key Takeaways

  • grpc.NewClient creates a connection (grpc.Dial is deprecated)
  • insecure.NewCredentials() is required for local dev — gRPC won't connect without an explicit transport security choice
  • pb.NewLinkServiceClient(conn) gives you a typed client from the generated code
  • Always pass a context with a timeout to prevent hanging calls
  • Use status.FromError to extract gRPC status codes from errors
  • One connection handles many concurrent RPCs — no connection pool needed
  • The connection reconnects automatically if the server restarts

💻 Examples

Complete examples for this lesson. Copy and run locally.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue

© 2026 ByteLearn.dev. Free courses for developers. · Privacy