Getting started with gRPC-Go

1. Introduction

In this codelab, you'll use gRPC-Go to create a client and server that form the foundation of a route-mapping application written in Go.

By the end of the tutorial, you will have a client that connects to a remote server using gRPC to get the name or postal address of what's located at specific coordinates on a map. A fully fledged application might use this client-server design to enumerate or summarize points of interest along a route.

The service is defined in a Protocol Buffers file, which will be used to generate boilerplate code for the client and server so that they can communicate with each other, saving you time and effort in implementing that functionality.

This generated code takes care of not only the complexities of the communication between the server and client, but also data serialization and deserialization.

What you'll learn

  • How to use Protocol Buffers to define a service API.
  • How to build a gRPC-based client and server from a Protocol Buffers definition using automated code generation.
  • An understanding of client-server communication with gRPC.

This codelab is aimed at Go developers new to gRPC or seeking a refresher of gRPC, or anyone else interested in building distributed systems. No prior gRPC experience is required.

2. Before you begin

Prerequisites

Make sure you have installed the following:

  • The Go toolchain version 1.24.5 or later. For installation instructions, see Go's Getting started.
  • The protocol buffer compiler, protoc, version 3.27.1 or later. For installation instructions, see the compiler's installation guide.
  • The protocol buffer compiler plugins for Go and gRPC. To install these plugins, run the following commands:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Update your PATH variable so that the protocol buffer compiler can find the plugins:

export PATH="$PATH:$(go env GOPATH)/bin"

Get the code

So that you don't have to start entirely from scratch, this codelab provides a scaffold of the application's source code for you to complete. The following steps will show you how to finish the application, including using the protocol buffer compiler plugins to generate the boilerplate gRPC code.

Download this source code as a .ZIP archive from GitHub and unpack its contents.

Alternatively, the completed source code is available on GitHub if you want to skip typing in an implementation.

3. Define the service

Your first step is to define the application's gRPC service, its RPC method, and its request and response message types using Protocol Buffers. Your service will provide:

  • An RPC method called GetFeature that the server implements and the client calls.
  • The message types Point and Feature that are data structures exchanged between the client and server when using the GetFeature method. The client provides map coordinates as a Point in its GetFeature request to the server, and the server replies with a corresponding Feature that describes whatever is located at those coordinates.

This RPC method and its message types will all be defined in the routeguide/route_guide.proto file of the provided source code.

Protocol Buffers are commonly known as protobufs. For more information on gRPC terminology, see gRPC's Core concepts, architecture, and lifecycle.

Message types

In the routeguide/route_guide.proto file of the source code, first define the Point message type. A Point represents a latitude-longitude coordinate pair on a map. For this codelab, use integers for the coordinates:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

The numbers 1 and 2 are unique ID numbers for each of the fields in the message structure.

Next, define the Feature message type. A Feature uses a string field for the name or postal address of something at a location specified by a Point:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Service method

The route_guide.proto file has a service structure named RouteGuide that defines one or more methods provided by the application's service.

Add the rpc method GetFeature inside the RouteGuide definition. As explained earlier, this method will look up the name or address of a location from a given set of coordinates, so have GetFeature return a Feature for a given Point:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

This is a unary RPC method: a simple RPC where the client sends a request to the server and waits for a response to come back, just like a local function call.

4. Generate the client and server code

Next, generate the boilerplate gRPC code for both the client and server from the .proto file using the protocol buffer compiler. In the routeguide directory, run:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

This command generates the following files:

  • route_guide.pb.go, which contains functions to create the application's message types and access their data.
  • route_guide_grpc.pb.go, which contains functions the client uses to call the service's remote gRPC method, and functions used by the server to provide that remote service.

Next, we'll implement the GetFeature method on the server-side, so that when the client sends a request, the server can reply with an answer.

5. Implement the service

The GetFeature function on the server side is where the main work is done: this takes a Point message from the client and returns in a Feature message the corresponding location information from a list of known places. Here's the function's implementation in server/server.go:

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

When this method is invoked following a request from a remote client, the function is passed a Context object describing the RPC call, and a Point protocol buffer object from that client request. The function returns a Feature protocol buffer object for the looked-up location and an error as necessary.

In the method, populate a Feature object with the appropriate information for the given Point, and then return it along with a nil error to tell gRPC that you've finished dealing with the RPC and that the Feature object can be returned to the client.

The GetFeature method requires a routeGuideServer object to be created and registered so that requests from clients for location look-ups can be routed to that function. This is done in main():

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

Here's what is happening in main(), step by step:

  1. Specify the TCP port to use to listen for remote client requests, using lis, err := net.Listen(...). By default, the application uses TCP port 50051 as specified by the variable port or by passing the --port switch on the command line when running the server. If the TCP port can't be opened, the application ends with a fatal error.
  2. Create an instance of the gRPC server using grpc.NewServer(...), naming this instance grpcServer.
  3. Create a pointer to routeGuideServer, a structure representing the application's API service, naming the pointer s.
  4. Use s.loadFeatures() to populate the array s.savedFeatures with locations that can be looked up via GetFeature.
  5. Register the API service with the gRPC server so that RPC calls to GetFeature are routed to the appropriate function.
  6. Call Serve() on the server with our port details to do a blocking wait for client requests; this continues until the process is killed or Stop() is called.

The function loadFeatures() gets its coordinates-to-location mappings from server/testdata.go.

6. Create the client

Now edit client/client.go, which is where you'll implement the client code.

To call the remote service's methods, we first need to create a gRPC channel to communicate with the server. We create this by passing the server's target URI string (which in this case is simply the address and port number) to grpc.NewClient() in the client's main() function as follows:

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

The server's address, defined by the variable serverAddr, is by default localhost:50051, and can be overridden by the --addr switch on the command line when running the client.

If the client needs to connect to a service that requires authentication credentials, such as TLS or JWT credentials, the client can pass a DialOptions object as a parameter to grpc.NewClient that contains the required credentials. The RouteGuide service doesn't require any credentials.

Once the gRPC channel is set up, we need a client stub to perform RPCs via Go function calls. We get that stub using the NewRouteGuideClient method provided by the route_guide_grpc.pb.go file generated from the application's .proto file.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

Call service methods

In gRPC-Go, RPCs operate in a blocking/synchronous mode, which means that the RPC call waits for the server to respond, and will either return a response or an error.

Simple RPC

Calling the simple RPC GetFeature is nearly as straightforward as calling a local method, in this case client.GetFeature:

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

The client calls the method on the stub created earlier. For the method's parameters, the client creates and populates a Point request protocol buffer object. You also pass a context.Context object which lets us change our RPC's behavior if necessary, such as defining a time limit for the call or canceling an RPC in flight. If the call doesn't return an error, the client can read the response information from the server from the first return value:

log.Println(feature)

In all, the client's main() function should look like this:

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. Try it out

Confirm the server and client are working with each other correctly by executing the following commands in the application's working directory:

  1. Run the server in one terminal:
cd server
go run .
  1. Run the client from another terminal:
cd client
go run .

You'll see output like this, with timestamps omitted for clarity:

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. What's next