How to Call gRPC Methods Dynamically in Go

gRPC (Google Remote Procedure Call) is a high-performance, open-source framework for making remote procedure calls. Unlike traditional REST APIs that use HTTP/1.1 and JSON, gRPC leverages HTTP/2 and Protocol Buffers for more efficient communication between distributed systems. This modern approach offers significant advantages in terms of performance, type safety, and code generation capabilities. When working with gRPC, you need to understand the fundamental components and workflow that differentiate it from other API communication methods. The process begins with defining your service interface using Protocol Buffers (protobuf) in a proto file, which serves as a contract between client and server. This definition is then compiled into language-specific code that handles all the underlying communication complexities. You can make gRPC calls dynamically in two ways: using server reflection or providing uncompiled proto files. Dynamic Calls Using gRPC Server Reflection Typically, services are registered with a gRPC server immediately after creation, as shown in the helloworld example provided by grpc team: import ( "google.golang.org/grpc" ) type server struct { helloworld.UnimplementedGreeterServer } func main() { // ... s := grpc.NewServer() helloworld.RegisterGreeterServer(s, &server{}) // ... } To enable server reflection, register the reflection service by calling the Register function from the reflection package as follows. import ( "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) type server struct { helloworld.UnimplementedGreeterServer } func main() { // ... s := grpc.NewServer() helloworld.RegisterGreeterServer(s, &server{}) reflection.Register(s) // ... } After starting the server, you can check the new service by using Kreya's (https://kreya.app/) server reflection importing. The picture below shows this result with server reflection service: Calling gRPC dynamically with reflection involves two steps: first, a request to the ServerReflectionInfo stream retrieves proto files (FileDescriptors). The sequence diagram illustrates this process. Note that for every service call, one call must be made to the reflection service to fetch the proto file. The following code implements the process shown in the diagram above. The function grpcurl.DescriptorSourceFromServer retrieves the file descriptor from the reflection service. The call to the target method is done by grpcurl.InvokeRPC function. package main import ( "bytes" "context" "log" "strings" "github.com/fullstorydev/grpcurl" "github.com/jhump/protoreflect/grpcreflect" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func main() { // Inputs serverAddr := "localhost:50051" methodFullName := "helloworld.Greeter/SayHello" jsonRequest := `{ "name": "goodbye, hello goodbye, you say stop and I say go go..." }` // Output buffer var output bytes.Buffer // Create gRPC channel grpcChannel, err := grpc.Dial(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("Failed to dial server: %v", err) } defer grpcChannel.Close() // Create reflection client reflectionClient := grpcreflect.NewClient(context.Background(), grpcreflect.NewClientV1(grpcChannel)) defer reflectionClient.Reset() // Use grpcurl to get the method descriptor descriptorSource := grpcurl.DescriptorSourceFromServer(context.Background(), reflectionClient) // Prepare formatter for the response options := grpcurl.FormatOptions{EmitJSONDefaultFields: true} jsonRequestReader := strings.NewReader(jsonRequest) rf, formatter, err := grpcurl.RequestParserAndFormatter(grpcurl.Format("json"), descriptorSource, jsonRequestReader, options) if err != nil { log.Fatalf("Failed to construct request parser and formatter: %v", err) } eventHandler := &grpcurl.DefaultEventHandler{ Out: &output, Formatter: formatter, VerbosityLevel: 0, } headers := []string{} err = grpcurl.InvokeRPC(context.Background(), descriptorSource, grpcChannel, methodFullName, headers, eventHandler, rf.Next) if err != nil { log.Fatalf("RPC call failed: %v", err) } log.Println("Received output:") log.Print(output.String()) } The images below show the console outputs from the client and server runs, respectively. You can seem from the log output one call for the stream and other for the target service. Dynamic Calls Using Proto Files If you have access to the proto file, you can call the target service directly by changing the function grpcurl.DescriptorSourceFromServer to grpcurl.DescriptorSourceFromProtoFiles . In this way, the steps needed to call the service are simplified as shown in the sequence diagram below. The

May 10, 2025 - 22:18
 0
How to Call gRPC Methods Dynamically in Go

gRPC (Google Remote Procedure Call) is a high-performance, open-source framework for making remote procedure calls. Unlike traditional REST APIs that use HTTP/1.1 and JSON, gRPC leverages HTTP/2 and Protocol Buffers for more efficient communication between distributed systems. This modern approach offers significant advantages in terms of performance, type safety, and code generation capabilities.

When working with gRPC, you need to understand the fundamental components and workflow that differentiate it from other API communication methods. The process begins with defining your service interface using Protocol Buffers (protobuf) in a proto file, which serves as a contract between client and server. This definition is then compiled into language-specific code that handles all the underlying communication complexities.

You can make gRPC calls dynamically in two ways: using server reflection or providing uncompiled proto files.

Dynamic Calls Using gRPC Server Reflection

Typically, services are registered with a gRPC server immediately after creation, as shown in the helloworld example provided by grpc team:

import (
    "google.golang.org/grpc"
)

type server struct {
    helloworld.UnimplementedGreeterServer
}

func main() {
    // ...
    s := grpc.NewServer()
    helloworld.RegisterGreeterServer(s, &server{})
    // ...
}

To enable server reflection, register the reflection service by calling the Register function from the reflection package as follows.

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

type server struct {
    helloworld.UnimplementedGreeterServer
}

func main() {
    // ...
    s := grpc.NewServer()
    helloworld.RegisterGreeterServer(s, &server{})
    reflection.Register(s)
    // ...
}

After starting the server, you can check the new service by using Kreya's (https://kreya.app/) server reflection importing. The picture below shows this result with server reflection service:

Kreya Server reflection

Calling gRPC dynamically with reflection involves two steps: first, a request to the ServerReflectionInfo stream retrieves proto files (FileDescriptors). The sequence diagram illustrates this process. Note that for every service call, one call must be made to the reflection service to fetch the proto file.

server reflection sequence diagram

The following code implements the process shown in the diagram above. The function grpcurl.DescriptorSourceFromServer retrieves the file descriptor from the reflection service. The call to the target method is done by grpcurl.InvokeRPC function.

package main

import (
    "bytes"
    "context"
    "log"
    "strings"

    "github.com/fullstorydev/grpcurl"
    "github.com/jhump/protoreflect/grpcreflect"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    // Inputs
    serverAddr := "localhost:50051"
    methodFullName := "helloworld.Greeter/SayHello"
    jsonRequest := `{ "name": "goodbye, hello goodbye, you say stop and I say go go..." }`

    // Output buffer
    var output bytes.Buffer

    // Create gRPC channel
    grpcChannel, err := grpc.Dial(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("Failed to dial server: %v", err)
    }
    defer grpcChannel.Close()

    // Create reflection client
    reflectionClient := grpcreflect.NewClient(context.Background(), grpcreflect.NewClientV1(grpcChannel))
    defer reflectionClient.Reset()

    // Use grpcurl to get the method descriptor
    descriptorSource := grpcurl.DescriptorSourceFromServer(context.Background(), reflectionClient)

    // Prepare formatter for the response
    options := grpcurl.FormatOptions{EmitJSONDefaultFields: true}
    jsonRequestReader := strings.NewReader(jsonRequest)
    rf, formatter, err := grpcurl.RequestParserAndFormatter(grpcurl.Format("json"), descriptorSource, jsonRequestReader, options)
    if err != nil {
        log.Fatalf("Failed to construct request parser and formatter: %v", err)
    }
    eventHandler := &grpcurl.DefaultEventHandler{
        Out:            &output,
        Formatter:      formatter,
        VerbosityLevel: 0,
    }

    headers := []string{}

    err = grpcurl.InvokeRPC(context.Background(), descriptorSource, grpcChannel, methodFullName, headers, eventHandler, rf.Next)
    if err != nil {
        log.Fatalf("RPC call failed: %v", err)
    }
    log.Println("Received output:")
    log.Print(output.String())
}

The images below show the console outputs from the client and server runs, respectively. You can seem from the log output one call for the stream and other for the target service.

server reflection grpc call

Dynamic Calls Using Proto Files

If you have access to the proto file, you can call the target service directly by changing the function grpcurl.DescriptorSourceFromServer to grpcurl.DescriptorSourceFromProtoFiles . In this way, the steps needed to call the service are simplified as shown in the sequence diagram below.

proto call sequence diagram

The results from the server and client can be seen below, where the stream is not called anymore.

proto file call output

Conclusion

There are two effective approaches for making dynamic gRPC calls. Server reflection offers several advantages: it eliminates the need to maintain proto files on the client side, enables dynamic discovery of services, and simplifies integration testing and debugging tools. With reflection, clients can automatically discover and interact with services without prior knowledge of their interfaces.

However, server reflection does have drawbacks. Each service call requires an additional reflection call, which adds network overhead. While you can mitigate this by caching file descriptors after the first reflection call. Reflection may also expose additional server details that could potentially reduce security.

Alternatively, using proto files directly provides a more efficient approach with fewer network calls, but requires keeping the client's proto files synchronized with the server.

The complete code examples from this article are available at https://github.com/dmo2000/grpc-dynamic-calls.