Как собирал ручки gRPC сервиса?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Процесс сборки gRPC-хэндлеров в Go: от прототипа до production
Сборка gRPC-хэндлеров — это многоэтапный процесс, сочетающий кодогенерацию, DI-контейнеры, middleware-цепочки и организацию бизнес-логики. Вот как я обычно выстраиваю этот процесс.
1. Генерация кода из Protobuf-определений
Первым шагом всегда является создание .proto файлов с описанием сервисов и сообщений. После этого используется компилятор protoc с Go-плагинами:
# Пример команды генерации
protoc \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
api/v1/*.proto
Получаем два файла:
*.pb.go— структуры сообщений*_grpc.pb.go— интерфейсы сервера и клиента
Важный момент: начиная с gRPC-GO v1.64+ появилась опция require_unimplemented_servers=false, которую я обычно отключаю для обратной совместимости.
2. Реализация сервера и структурирование хэндлеров
Я следую принципу "один сервис — одна реализация". Пример структуры:
package handler
import (
"context"
pb "myproject/api/v1"
)
type UserService struct {
pb.UnimplementedUserServiceServer // Для совместимости
repo user.Repository
logger *zap.Logger
metrics prometheus.Counter
}
func NewUserService(repo user.Repository, logger *zap.Logger) *UserService {
return &UserService{
repo: repo,
logger: logger,
}
}
func (s *UserService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
// Бизнес-логика с использованием зависимостей
user, err := s.repo.GetByID(ctx, req.Id)
if err != nil {
s.logger.Error("failed to get user", zap.Error(err))
s.metrics.Inc()
return nil, status.Error(codes.NotFound, "user not found")
}
return &pb.UserResponse{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}, nil
}
3. Интеграция зависимостей через DI-контейнер
Для инъекции зависимостей я предпочитаю ручной DI или легковесные библиотеки типа wire или fx:
// Пример с Google Wire
func InitializeServer() (*grpc.Server, error) {
wire.Build(
database.NewPostgres,
redis.NewClient,
repository.NewUserRepo,
handler.NewUserService,
NewGRPCServer, // Фабрика сервера
)
return &grpc.Server{}, nil
}
func NewGRPCServer(userService *handler.UserService) *grpc.Server {
server := grpc.NewServer(
grpc.UnaryInterceptor(chainUnaryInterceptors),
grpc.StreamInterceptor(chainStreamInterceptors),
)
pb.RegisterUserServiceServer(server, userService)
return server
}
4. Регистрация middleware/interceptors
Интерцепторы — критически важный слой для кросс-резаных задач:
func chainUnaryInterceptors(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Порядок имеет значение!
chain := grpc_middleware.ChainUnaryServer(
loggingInterceptor,
metricsInterceptor,
recoveryInterceptor,
authInterceptor,
validationInterceptor,
rateLimitInterceptor,
)
return chain(ctx, req, info, handler)
}
func loggingInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
duration := time.Since(start)
zap.L().Info("gRPC request",
zap.String("method", info.FullMethod),
zap.Duration("duration", duration),
zap.Error(err),
)
return resp, err
}
5. Рефлексия и health-чеки
Для отладки и мониторинга обязательно добавляю:
import (
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
func enableReflectionAndHealth(srv *grpc.Server) {
// Рефлексия для grpcurl и тестирования
reflection.Register(srv)
// Health checks для Kubernetes и load balancers
healthServer := health.NewServer()
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(srv, healthServer)
}
6. Запуск сервера с graceful shutdown
Финальный этап — корректный запуск и остановка:
func RunServer(addr string, srv *grpc.Server) error {
lis, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to listen: %w", err)
}
// Graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
go func() {
if err := srv.Serve(lis); err != nil && err != grpc.ErrServerStopped {
zap.L().Fatal("gRPC server failed", zap.Error(err))
}
}()
<-stop
zap.L().Info("Shutting down gRPC server...")
srv.GracefulStop() // Даем завершить текущие запросы
// или srv.Stop() для быстрой остановки
return nil
}
Ключевые практики, которые я выработал:
- Строгая иерархия слоев — transport (gRPC) → business logic → repository
- Полное покрытие ошибок — всегда возвращаю gRPC-статусы через
status.Error(), а не обычныеerror - Контекст — прокидываю через все слои для таймаутов, трейсинга и cancellation
- Метрики и логирование — обязательно в interceptors, а не в каждом хэндлере
- Тестирование — использую
bufconnдля in-memory тестирования gRPC-сервисов без сети - Версионирование API — через отдельные папки
api/v1,api/v2в proto-определениях
Такой подход обеспечивает масштабируемость, тестируемость и наблюдаемость gRPC-сервисов. В production-среде это позволяет быстро локализовать проблемы, добавлять новые функции без breaking changes и поддерживать высокую нагрузку.