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
andFeature
that are data structures exchanged between the client and server when using theGetFeature
method. The client provides map coordinates as aPoint
in itsGetFeature
request to the server, and the server replies with a correspondingFeature
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:
- Specify the TCP port to use to listen for remote client requests, using
lis, err := net.Listen(...)
. By default, the application uses TCP port50051
as specified by the variableport
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. - Create an instance of the gRPC server using
grpc.NewServer(...)
, naming this instancegrpcServer
. - Create a pointer to
routeGuideServer
, a structure representing the application's API service, naming the pointers.
- Use
s.loadFeatures()
to populate the arrays.savedFeatures
with locations that can be looked up viaGetFeature
. - Register the API service with the gRPC server so that RPC calls to
GetFeature
are routed to the appropriate function. - Call
Serve()
on the server with our port details to do a blocking wait for client requests; this continues until the process is killed orStop()
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:
- Run the server in one terminal:
cd server go run .
- 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
- Learn how gRPC works in Introduction to gRPC and Core concepts
- Work through the Basics tutorial
- Explore the API reference