uber/fx + gRPC
В своих решениях я использую IoC
-контейнер fx
1 от Uber
. Существуют разные лагери сторонников и противников
подобных решений, но мы сконцентрируемся именно на теме применения fx
.
Если вы не знакомы с основами применения, я рекомендую выполнить официальный Quick Start
2. Он поможет
начать ориентироваться в решении.
Итак, задача: зарегистрировать и запустить 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
, и это становится заметным по мере роста и развития вашего проекта.