The gRPC code generation creates a complete client and server implementation
that handles all of the transport-level concerns. Goa generates a Protobuf
definition for each service and automatically calls protoc
to generate the
server and client low-level code. Goa also generates code that leverages the
Protobuf-generated code to create a high-level gRPC server and client
implementation.
The protobuf service definition is generated in
gen/grpc/<name of service>/pb/goagen_<name of api>_<name of service>.proto
:
syntax = "proto3";
package calc;
service Calc {
rpc Add (AddRequest) returns (AddResponse);
rpc Multiply (MultiplyRequest) returns (MultiplyResponse);
}
message AddRequest {
int64 a = 1;
int64 b = 2;
}
message AddResponse {
int64 result = 1;
}
// ...more messages...
This protobuf definition is used to generate the low-level gRPC code via the
protoc
compiler, which Goa invokes automatically during code generation.
Goa generates a complete gRPC server implementation in
gen/grpc/<name of service>/server/server.go
that expose the service methods
using gRPC. The server can be instantiated using the generated New
function
which accepts the service endpoints and optional unary handler. If no unary
handler is provided, the server will use the default handler provided by Goa.
// New instantiates the server struct with the calc service endpoints.
func New(e *calc.Endpoints, uh goagrpc.UnaryHandler) *Server {
return &Server{
AddH: NewAddHandler(e.Add, uh),
MultiplyH: NewMultiplyHandler(e.Multiply, uh),
}
}
In the code above goagrpc
refers to the Goa gRPC package located at
goa.design/goa/v3/grpc
. The UnaryHandler
type is a function that takes a
context and a request and returns a response and an error. If the service
exposes streaming methods, New
also accepts a streaming handler.
The Server
struct exposes fields that can be used to modify individual
handlers or apply middleware to specific endpoints:
// Server lists the calc service endpoint gRPC handlers.
type Server struct {
AddH goagrpc.UnaryHandler
MultiplyH goagrpc.UnaryHandler
// ... Private fields ...
}
The rest of the server file implementes the gRPC server handlers for each service method. These handlers are responsible for decoding the request, calling the service method, and encoding the response and error.
The service interface and endpoint layers are the core building blocks of your Goa-generated service. Your main package will use these layers to create the transport-specific server and client implementations, allowing you to run your service and interact with it using gRPC:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"github.com/<your username>/calc"
gencalc "github.com/<your username>/calc/gen/calc"
genpb "github.com/<your username>/calc/gen/grpc/calc/pb"
gengrpc "github.com/<your username>/calc/gen/grpc/calc/server"
)
func main() {
svc := calc.New() // Your service implementation
endpoints := gencalc.NewEndpoints(svc) // Create the service endpoints
svr := grpc.NewServer(nil) // Create the gRPC server
gensvr := gengrpc.New(endpoints, nil) // Create the server implementation
genpb.RegisterCalcServer(svr, genserver) // Register the server with gRPC
lis, _ := net.Listen("tcp", ":8080") // Start the gRPC server listener
svr.Serve(lis) // Start the gRPC server
}
Goa generates a complete gRPC client implementation in
gen/grpc/<name of service>/client/client.go
. Similar to the HTTP client, it
provides methods that create Goa endpoints for each service method, which can
then be wrapped into transport-agnostic client implementations.
The generated NewClient
function creates an object that can be used to create
transport-agnostic client endpoints:
// NewClient instantiates gRPC client for all the calc service endpoints.
func NewClient(cc *grpc.ClientConn, opts ...grpc.CallOption) *Client {
return &Client{
grpccli: calcpb.NewCalcClient(cc),
opts: opts,
}
}
The function requires a gRPC connection (*grpc.ClientConn
) which is used to
create the low-level protobuf client. It also accepts optional gRPC call options
(...grpc.CallOption
) which are used to configure the gRPC client.
The instantiated Client
struct exposes methods that build transport-agnostic
endpoints:
// Add returns an endpoint that makes gRPC requests to the calc service
// add server.
func (c *Client) Add() goa.Endpoint
// Multiply returns an endpoint that makes gRPC requests to the calc service
// multiply server.
func (c *Client) Multiply() goa.Endpoint
Here’s an example of how to create and use the gRPC client for the calc
service:
package main
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
gencalc "github.com/<your username>/calc/gen/calc"
genclient "github.com/<your username>/calc/gen/grpc/calc/client"
)
func main() {
// Create the gRPC connection
conn, err := grpc.Dial("localhost:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Create the gRPC client
grpcClient := genclient.NewClient(conn)
// Create the endpoint client
client := gencalc.NewClient(
grpcClient.Add(), // Add endpoint
grpcClient.Multiply(), // Multiply endpoint
)
// Call the service methods
result, err := client.Add(context.Background(), &gencalc.AddPayload{A: 1, B: 2})
if err != nil {
log.Fatal(err)
}
log.Printf("1 + 2 = %d", result)
}
This example shows how to:
The gRPC client handles all transport-level concerns while the endpoint client provides a clean interface for making service calls.
The generated gRPC client provides a convenient way to interact with the service using gRPC. The client handles all the complexities of gRPC communication while providing a consistent interface that matches other transport implementations.