Как я структурирую сервисы в Go

Итак, вы создаете новую службу, у вас есть один файл main.go, несколько обработчиков HTTP и все в порядке. Однако вы начинаете добавлять больше маршрутов и начинаете разделять некоторые функции на разные файлы, возможно, даже создавая несколько пакетов здесь и там, но не слишком уверены, как это будет работать в долгосрочной перспективе, и хотите что-то, что имеет смысл в качестве вашей службы. растет.

Я сталкивался с этим несколько раз за эти годы, и после прочтения нескольких статей, сообщений в блогах и частей чистой архитектуры Роберта Мартина я нашел общую структуру, которая работает для меня, поэтому я подумал, что поделюсь ею. . Стоит отметить, что это может не совсем соответствовать вашему варианту использования, особенно если у вас очень простой сервис - одного файла main.go и нескольких пакетов, вероятно, будет достаточно.

Давай займемся этим!

Обзор услуг

Go-Service
- cmd/
  - api/
- pkg/
  - api/
  - db/
  - services/
    - serviceA/
    - serviceB/
    - ...
  - utils/
- docker-compose.yml
- Dockerfile
- Makefile
- go.mod
- go.sum
- <environment>.env
- README.md
- ...

Эта структура состоит из трех основных частей: root, cmd и pkg. Я рассмотрю, за что должен отвечать каждый из этих каталогов, а затем мы возьмем Посмотрите внимательнее, как организована каждая из служб (pkg/services/...).

Корень

Здесь мне нравится хранить все, что используется для работы с исходным кодом, что помогает его запустить и запустить, например, инструменты сборки, файлы конфигурации, управление зависимостями и т. Д. Это также дает читателю / разработчику хорошую отправную точку - все, что они необходимость запуска службы находится прямо в корне проекта.

Cmd

Все, что я хочу запустить как часть нашего сервиса, войдет сюда. Например. API, задания cron и т. д. По сути, все, что будет скомпилировано в двоичный файл, будет иметь здесь основной пакет, в котором мы инициализируем конфигурацию и пакеты, необходимые для запуска любой части службы.

Упак.

В нем содержится суть проекта: пакеты, определяющие поведение наших сервисов. Позвольте мне быстро объяснить каждый из них.

  • api /
    Здесь я определяю, как подключить API, инициализируя БД, службы, HTTP-маршрутизатор + промежуточное ПО, и определяю конфигурацию, необходимую для запуска API. Обычно у меня есть одна Start(cfg *Config) функция, которая вызывается из cmd/api/main.go.
  • db /
    Само собой разумеется - здесь находится логика подключения и миграции. Я также стараюсь помещать сюда любые папки / файлы миграции.
  • utils /
    Сюда я брошу любой пакет, связанный с обработкой запросов, ведением журнала, настраиваемым промежуточным программным обеспечением и т. д. Я не самый большой поклонник этого имени, но я не пока не попал ни на что лучше.
  • services /
    Это требует немного большего объяснения, потому что я структурирую каждую из этих служб определенным образом. В общем, каждый из этих пакетов определяет функцию службы (структурирование по функциям, а не по функциям).

Услуги

Давайте на примере рассмотрим, как они устроены. Допустим, мы хотим создать службу, которая позволяет нам хранить и создавать статьи, это будет выглядеть примерно так:

...
- Services/
  - Article/
    - store/
      - repo.go
    - transport/
      - http.go
    - article.go
    - errors.go
    - models.go

Мы разделяем логику хранения и транспортировки данных на отдельные пакеты - это помогает нам сосредоточиться на том, что важно при написании нашей бизнес-логики, поскольку нам не обязательно заботиться о том, как мы храним данные или как они доставляются вызывающему. Кроме того, скажем, мы хотели изменить базовую базу данных, пока мы придерживаемся определенного интерфейса, мы можем поменять его, не затрагивая остальную часть приложения (простой пример принципа инверсии зависимостей).

Ошибки errors.go и models.go говорят сами за себя, поэтому давайте посмотрим на то, что у нас есть в article.go:

Здесь главное отметить, что мы передаем в службу экземпляр Repo (хранилище). Это то, что усиливает то разделение, о котором мы только что говорили, а также помогает нам в тестировании. При тестировании этой службы мы можем просто создать макет, который удовлетворяет интерфейсу Repo, что позволяет нам обойтись без раскрутки базы данных только для запуска модульных тестов службы.

Как мы подаем это внешнему миру?

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

Помните ту функцию Start, о которой я говорил ранее (в pkg/api)? Здесь мы активируем все наши службы и используем нашу конфигурацию:

И это почти все - на самом деле он просто пытается найти способ абстрагироваться от деталей настолько, чтобы ваш пакет было легко читать, не добавляя слишком большой сложности вашему проекту.

Заключение

Есть много способов организовать проект, и это самый полезный. Разделение сервисов по функциям помогает определить контекстные границы и навигацию по коду при внесении будущих изменений. В рамках сервиса, разделенного на уровни транспорта, сервисной / бизнес-логики и хранилища, можно четко разделить проблемы и упростить тестирование. Не усложняйте! Если вы можете избежать наказания за меньшее, сделайте это во что бы то ни стало.

Я надеюсь, что некоторые из вас сочтут это полезным. Если вам интересно увидеть весь исходный код, вы можете найти его здесь.