Где в структуре Go-проекта принято размещать транспортный слой и какова его роль?

Ответ

Транспортный слой отвечает за взаимодействие приложения с внешним миром (например, прием HTTP-запросов или gRPC-вызовов). Его принято изолировать от бизнес-логики (сервисного слоя).

Расположение в проекте

Чаще всего его размещают в директории internal, чтобы избежать импорта из других проектов. Популярные названия:

  • internal/transport (общее название)
  • internal/handler или internal/handlers
  • internal/api

Внутри этих папок создают поддиректории для каждого типа транспорта:

/my-project
└── internal/
    ├── service/        # Бизнес-логика
    └── transport/      # Транспортный слой
        ├── http/
        │   ├── handler.go    # Обработчики
        │   ├── middleware.go # Middleware
        │   └── router.go     # Регистрация роутов
        └── grpc/
            └── server.go

Роль и обязанности транспортного слоя:

  1. Прием и разбор (parsing) входящих запросов (HTTP, gRPC и т.д.).
  2. Валидация данных запроса (заголовки, тело, параметры).
  3. Преобразование (mapping) данных из формата запроса (например, JSON) во внутренние структуры (DTO или модели для сервисного слоя).
  4. Вызов соответствующего метода сервисного слоя с передачей ему валидированных данных.
  5. Обработка ошибок, возвращаемых сервисным слоем, и преобразование их в корректные ответы (например, HTTP-статусы 400, 404, 500).
  6. Сериализация данных ответа (например, в JSON) и отправка клиенту.

Пример HTTP-обработчика:

package http

import "net/http"

// service - интерфейс бизнес-логики, который мы будем вызывать.
type service interface {
    GetUser(id int) (*User, error)
}

type Handler struct {
    service service
}

func NewHandler(s service) *Handler {
    return &Handler{service: s}
}

// GetUser обрабатывает запрос на получение пользователя.
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    // 1. Парсинг и валидация ID из URL
    id, err := parseID(r)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    // 2. Вызов бизнес-логики
    user, err := h.service.GetUser(id)
    if err != nil {
        // 3. Обработка ошибок от сервиса
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    // 4. Сериализация и отправка ответа
    respondJSON(w, http.StatusOK, user)
}