Skip to content
S
distributed systems

Building a gRPC Client in Go

Updated · originally #grpc#go4 min read

Introduction

In this article, we will build a gRPC client in Go from the same Protocol Buffers contract as the Go gRPC server. We will generate the client stub, create a reusable gRPC channel, set a deadline, and call the unary GetBookList RPC.

The example uses plaintext transport so it is easy to run locally. That is a development choice, not a production default.

Plan

The plan for this article is as follows.

  1. Scaffold the client-side stubs and Go module.
  2. Implement the gRPC client.
  3. Communicate with the server.

In a nutshell, we will be generating the client for the server we built in our previous post.

building-grpc-client-go-1.png

In the above diagram, we will look at how to achieve the components on the left side.

💡 As always, the completed code can be found at: https://github.com/sahansera/go-grpc/tree/main/client

Creating client-side Stubs and Go modules

We will be using the same Protobuf files that we generated in our previous step. If you haven’t seen that already head over to my previous post.

We will create a new folder called client at the root of the project and initialize it with a Go module.

Shell
mkdir client && cd client
go mod init bookshop/client

From the repository root, generate the Protobuf message types and gRPC client interface into the client module:

Shell
protoc --proto_path=proto proto/*.proto --go_out=client --go-grpc_out=client

This produces bookshop.pb.go for the messages and bookshop_grpc.pb.go for the generated client and server interfaces. Both are generated artifacts, so change the .proto file and rerun the command rather than editing these files by hand.

building-grpc-client-go-2.png

Implementing the gRPC client

Now that we have the modules we can go and implement the code for the client.

To call the server, create a gRPC ClientConn with grpc.NewClient. Despite the type name, it represents a reusable virtual channel that manages name resolution and underlying connections for generated clients.

💡 This local sample uses insecure.NewCredentials(). Configure TLS and authentication before connecting across an untrusted network.

The rough skeleton of the client code looks like the following.

main.go

Go
// ...
func main() {
	conn, err := grpc.NewClient(
		"localhost:8080",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatalf("failed to create client: %v", err)
	}
	defer conn.Close()

	client := pb.NewInventoryClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	bookList, err := client.GetBookList(ctx, &pb.GetBookListRequest{})
	if err != nil {
		log.Fatalf("failed to get book list: %v", err)
	}

	log.Printf("book list: %v", bookList)
}

The explanation of this code is as follows:

  1. grpc.NewClient creates the reusable gRPC channel for localhost:8080. It does not perform network I/O immediately; the first RPC causes the channel to connect.
  2. pb.NewInventoryClient wraps that channel with the strongly typed interface generated from bookshop.proto.
  3. context.WithTimeout bounds the call to five seconds. gRPC does not set a deadline by default, so using context.Background() directly could leave the caller waiting indefinitely during a dependency failure.
  4. GetBookList sends the generated request type and returns either the generated response or a gRPC status error.

Communicating with the server

From the repository root, start the server. It listens on port 8080:

Shell
make server

In another terminal, run the client:

Shell
make client

The terminal will print the two books returned by the server.

building-grpc-client-go-4

Nice! As you can see, the generated client makes the happy path small 🎉 The production work sits around that call: TLS and identity, realistic deadlines, status-aware retry policy, observability, and deciding which RPCs are safe to retry. A deadline limits waiting; it does not make a non-idempotent operation safe to repeat 😊

Conclusion

In this article, we reused the server’s Protocol Buffers contract to generate a Go client, created one reusable channel, and bounded the RPC with a deadline. That combination gives the caller a typed API without letting a failed dependency hold it forever.

I hope this article cleared up a lot of confusion that you had about gRPC. Please feel free to share your questions, thoughts, or feedback in the comments section below. Until next time 👋

References

Stay in the loop

Practical engineering notes, without the inbox noise.

Notes on distributed systems, resilient software, and engineering in the real world - usually once or twice a month.

Unsubscribe anytime. See what you get, or prefer a feed? Subscribe via RSS.