← Назад к вопросам

Как собирал ручки gRPC сервиса?

1.3 Junior🔥 121 комментариев
#Сетевые протоколы и API

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Процесс сборки 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
}

Ключевые практики, которые я выработал:

  1. Строгая иерархия слоев — transport (gRPC) → business logic → repository
  2. Полное покрытие ошибок — всегда возвращаю gRPC-статусы через status.Error(), а не обычные error
  3. Контекст — прокидываю через все слои для таймаутов, трейсинга и cancellation
  4. Метрики и логирование — обязательно в interceptors, а не в каждом хэндлере
  5. Тестирование — использую bufconn для in-memory тестирования gRPC-сервисов без сети
  6. Версионирование API — через отдельные папки api/v1, api/v2 в proto-определениях

Такой подход обеспечивает масштабируемость, тестируемость и наблюдаемость gRPC-сервисов. В production-среде это позволяет быстро локализовать проблемы, добавлять новые функции без breaking changes и поддерживать высокую нагрузку.

Как собирал ручки gRPC сервиса? | PrepBro