uber/fx + gRPC

I use Uber’s fx1 IoC container in my work. There are camps on both sides — fans and detractors of this kind of solution — but here we’ll focus specifically on using fx.

If you’re not familiar with the basics, I recommend going through the official Quick Start2. It’ll help you get your bearings.

So, the task: register and start a gRPC server inside an fx context.

For the example, let’s take the simplest possible spec:

syntax = "proto3";

package greeting;

option go_package = "./;greeting";

service Greeter {
  rpc Greet (GreetRequest) returns (GreetResponse) {}
}

message GreetRequest {
  string name = 1;
}

message GreetResponse {
  string message = 1;
}

After codegen we’ll need the gRPC server itself and an implementation of our service.

Wrap the gRPC server startup in a constructor that we’ll pass into fx.Provide():

func NewGRPCServer(lc fx.Lifecycle, logger *zap.Logger) *grpc.Server {
	srv := grpc.NewServer()

	lc.Append(fx.Hook{
		OnStart: func(ctx context.Context) error {
			logger.Info("Starting gRPC server")

			ln, err := net.Listen("tcp", ":9000")
			if err != nil {
				return err
			}

			go func() {
				if err := srv.Serve(ln); err != nil {
					logger.Error("Failed to Serve gRPC", zap.Error(err))
				}
			}()

			return nil
		},
		OnStop: func(ctx context.Context) error {
			logger.Info("Gracefully stopping gRPC server")

			srv.GracefulStop()

			return nil
		},
	})

	return srv
}

For the example, a bare-bones service implementation will do:

func (g *Greeter) Greet(_ context.Context, request *greeting.GreetRequest) (*greeting.GreetResponse, error) {
	g.logger.Info("Processing gRPC request", zap.String("name", request.Name))

	response := &greeting.GreetResponse{
		Message: fmt.Sprintf("Hello %s from autoinjected gRPC service!", request.Name),
	}

	return response, nil
}

func NewGreeter(logger *zap.Logger) *Greeter {
	return &Greeter{logger: logger}
}

type Greeter struct {
	logger *zap.Logger
}

The main complexity is in registering these components. Pay attention to the code block inside fx.Provide(). There we annotate the server with the grpc.ServiceRegistrar interface so that fx later knows what to call when registering our service implementations. And we annotate the service implementation itself with the generated gRPC interface for that service.

fx.Provide(
     zap.NewExample,

     // Annotate gRPC server instance as grpc.ServiceRegistrar
     fx.Annotate(
         grpcServer.NewGRPCServer,
         fx.As(new(grpc.ServiceRegistrar)),
     ),

     // Annotate service as generated interface
     fx.Annotate(
         grpcService.NewGreeter,
         fx.As(new(greeting.GreeterServer)),
     ),
),

In the fx.Invoke() block, we start the gRPC server and register our service implementation:

fx.Invoke(
    // Start annotated gRPC server
    func (grpc.ServiceRegistrar) {},

    // Invoke service registration using annotated gRPC server and annotated service
    greeting.RegisterGreeterServer,
),

All of this might look rather involved. So I put together a working example3 that you can run out of the box and get a feel for this approach.

fx is far simpler to use than wire, and that becomes more obvious as your project grows.