Generated gRPC Server and Client Code

Learn about the code generated by Goa, including service interfaces, endpoints, and transport layers.

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.

Protobuf Definition

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.

gRPC Server Implementation

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.

Putting It All Together

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
}

gRPC Client Implementation

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.

Client Creation

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.

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

Putting It All Together

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:

  1. Create a gRPC connection
  2. Create a gRPC client using the connection
  3. Wrap it in an endpoint client
  4. Make service calls using the client

The gRPC client handles all transport-level concerns while the endpoint client provides a clean interface for making service calls.

Conclusion

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.