uber/fx + gRPC

В своих решениях я использую IoC-контейнер fx1 от Uber. Существуют разные лагери сторонников и противников подобных решений, но мы сконцентрируемся именно на теме применения fx.

Если вы не знакомы с основами применения, я рекомендую выполнить официальный Quick Start2. Он поможет начать ориентироваться в решении.

Итак, задача: зарегистрировать и запустить gRPC-сервер в контексте fx.

Для примера возьмём самую простую спецификацию:

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;
}

После кодогенерации нам потребуется сам gRPC-сервер и реализация нашего сервиса.

Запуск gRPC-сервера обернём в конструктор, который будет передан в 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
}

Для примера нам будет достаточно предельно простой реализации нашего сервиса:

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
}

Основная сложность заключается в регистрации этих компонентов. Обратите внимание на блок кода в вызове fx.Provide(). В нём мы аннотируем его интерфейсом grpc.ServiceRegistrar, чтобы в дальнейшем fx понимал, кто должен быть вызван при регистрации реализации наших сервисов. А саму реализацию сервиса мы аннотируем сгенерированным gRPC интерфейсом этого сервиса.

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)),
     ),
),

В блоке fx.Invoke() запускаем gRPC-сервер и регистрируем нашу реализацию сервиса:

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

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

Всё это может показаться довольно сложным и запутанным. Поэтому я подготовил для вас работающий пример3, который можно запустить из коробки и разобраться в этом подходе.

fx применять сильно проще, чем wire, и это становится заметным по мере роста и развития вашего проекта.