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.