Где в структуре 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)
}

Ответ 18+ 🔞

А, слушай, вот это тема, про транспортный слой! Ну, это как будто твой дом, а этот слой — это такой консьерж ёбаный, или, там, швейцар в перчатках. Его задача — не пустить в твою квартиру (то есть, в бизнес-логику) всяких пиздюков с улицы, которые в грязных ботинках и с кривыми запросами.

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

Вот смотри, чтобы этот швейцар не разбежался по всему городу, его обычно сажают в директорию internal. Это типа закрытый клуб, для своих. Называть его можно по-разному, но суть одна:

/my-project
└── internal/
    ├── service/        # Тут у нас святая святых, бизнес-логика, где деньги считают
    └── transport/      # А это — проходная, КПП, ёпта!
        ├── http/       # Для тех, кто через браузер лезет
        │   ├── handler.go    # Охранники, которые проверяют пропуска
        │   ├── middleware.go # Это как рамки металлоискателя, все проходят
        │   └── router.go     # Расписание, кто куда идёт
        └── grpc/       # Для своих, по закрытому каналу связи
            └── server.go

Роль и обязанности этого самого швейцара-транспорта:

  1. Встретить и обнюхать. Принять запрос — HTTP, gRPC, да хоть голубиной почтой. Разобрать, что этот чувак вообще хочет.
  2. Проверить документы. Валидация, блядь! А тот ли это JSON? А тот ли заголовок? А не пришёл ли он пьяный? (статус 400 — Bad Request, иди проспись).
  3. Перевести с птичьего на человеческий. Преобразовать эти кривые данные из запроса во что-то понятное для твоего сервиса. Не тащить же в бизнес-логику сырой JSON, это ж моветон, ёпта!
  4. Позвать нужного человека. Вызвать соответствующий метод из service и сказать: «Вот, Иван Иваныч, разберитесь с этим гражданином».
  5. Уладить скандалы. Если сервис вернул ошибку («пользователь не найден», «денег нет»), транспорт должен не орать «ой всё!», а культурно сказать клиенту: «Извините, 404» или «Пшел вон, 403».
  6. Упаковать ответ и выпроводить. Взять результат от сервиса, аккуратно завернуть в JSON (или что там надо), дать статус 200 и вежливо закрыть дверь.

Вот, смотри, как это выглядит в коде, на примере охранника на HTTP-проходной:

package http

import "net/http"

// service — это наш главный по складу, который в internal/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!
    id, err := parseID(r)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest) // Неправильный пропуск, иди нахуй!
        return
    }

    // 2. Звоним главному: «Босс, тут чел с ID таким-то пришёл».
    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) // Вот твой пропуск, проходи.
}

Вот и вся магия, ебать мои старые костыли. Транспорт — это не умный, он просто посредник. Вся соль должна быть внутри, в сервисах. А этот слой — просто обёрточная бумага, чтобы не тыкать клиентам в лицо сырыми интерфейсами базы данных. Чистота архитектуры, ёпта!