Просто написать сложный код. Гораздо сложнее написать простой код.
Если сравнить подход с умными книжками, то ближе всего это похоже на “Сервисную многослойную архитектуру с элементами Clean Architecture или Hexagonal Architecture поверх JSON-RPC 2.0.”
А теперь попробуем описать это все простыми словами.
Кому не подойдет
Тем, кому нужен gRPC, и они точно понимают зачем (производительность, микросервисы на разных языках, стриминг данных)
Тем, кто любит REST и не готов посмотреть на что-то другое.
Основные подходы
Используемый тулинг (доступный из коробки)
Шаблон проекта для создания нового сервиса
Генерация слоя БД
Генерация слоя админки
Генерация документации по API c возможностью вызвать методы из UI
Генерация клиентов для различных языков
Снаружи
сервисы (не микросервисы и не монолит)
общаются между собой через JSON-RPC 2.0 (в основном асинхронно через brokersrv)
каждый сервис живет в своей схеме в общей базе данных
но может легко переехать в свою, если он не перекладывает данные между схемами
у сервиса может быть внешнее API (JSON-RPC 2.0) и внутреннее (JSON-RPC 2.0)
REST применим для работы с файлами
сервисов обычно немного
у сервисов есть стандартизированные подходы к метрикам/логам/ошибкам/трассировке
Внутри
обычно три слоя (но может быть два)
строгое направление зависимостей (от внешнего к внутреннему)
каждый слой знает только о слое ниже, но не наоборот
в каждом слое свои модели
поголовное отсутствие интерфейсов (используется только в бизнес-логике, если применимо)
Go proverbs: The bigger the interface, the weaker the abstraction.
зафиксированные библиотеки
работа с БД через mfd-generator (в основе go-pg)
HTTP: echo
RPC: zenrpc
cron: vmkteam/cron
slog: vmkteam/embedlog
Слои
pkg
Основные
db – слой с базой данных
\<domain\> – слой с бизнес-логикой, всегда равен названию проекта
rpc – слой с API
Дополнительные
app – приложение
client – клиенты до других сервисов
\<service name\> – пакет с другим сервисом, в котором лежит один файл с клиентом, полученный через rpcgen
\<otherpkg\> – любой другой самостоятельный пакет, который может использоваться в любых слоях
rest – если нужен rest как самостоятельный слой
vt – если нужно RPC для админки
Как не нужно называть пакеты
Не объясняет, что за домен или логика
domain, logic → \<domain\>, например newsportal
Слишком утилитарные названия, как util. Они не объясняют, что внутри пакета.
handlers, service, api – не объясняют, что за сервис → rest, rpc.
repositories, repository – нет четкого понимания, что за репо → db
Примеры сервисов
Задача 1. Простой сайт с админкой
Назовем проект newsportal. Делаем один сервис apisrv, в котором будут следующие пакеты:
cmd/
apisrv – main, инициализация коннектов и передача из в app
pkg/
app – приложение, которое умеет запускаться
db – слой с базой данных
newsportal – слой с бизнес-логикой
rpc – rpc для сайта
vt – rpc для админки
Процесс
Берем начальный шаблон gold-apisrv
Строим систему снизу вверх: db → newsportal → rpc
Тестируем последний слой с rpc (или с newsportal, если очень важно)
Используем соглашения по неймингу:
/v1/rpc/ – публичное API
/v1/vt/ – API для админки
/int/rpc/ – приватное API для межсервисного взаимодействия (в данном проекте отсутствует)
Задача 2. Сервис с большой бизнес логикой
Название проекта newsportal.
Очень большое API для мобильного приложения и сайта, с возможностью отправки пушей, почты.
Есть фоновые процессы бизнес-логики, и процессы синхронизации данных.
Определим список сервисов:
apisrv – сервис со всем публичным API
vtsrv – сервис с API для админки
notifysrv – сервис отправки пушей/почты
npsrv – (np = сокр. от newsportal) – сервис для фоновых процессов бизнес-логики
syncsrv – сервис для синхронизации данных, если он уже не помещается в npsrv
Межсервисное взаимодействие целиком асинхнронное, через brokersrv:
apisrv -> brokersrv <- notifysrv
База данных newsportal:
public – apisrv, vtsrv, npsrv
notifysrv – notifysrv
syncsrv – syncsrv (если данные надо переложить в public, то это возможно в рамках ответственности сервиса syncsrv)
flowchart LR
subgraph s1["newsportal db"]
_notifysrv[("notifysrv")]
_syncsrv[("syncsrv")]
public[("public")]
end
notifysrv["notifysrv"] --> brokersrv & _notifysrv
apisrv["apisrv"] --> brokersrv["brokersrv"] & public
vtsrv["vtsrv"] --> public
npsrv["npsrv"] --> public
syncsrv["syncsrv"] --> _syncsrv