Шаблон Чистой Архитектуры для приложений на Golang
Цель этого шаблона - показать принципы Чистой Архитектуры Роберта Мартина (дядюшки Боба):
- как структурировать проект и не дать ему превратиться в спагетти-код
- где хранить бизнес-логику, чтобы она оставалась независимой, чистой и расширяемой
- как не потерять контроль при росте проекта
Go-clean-template создан и поддерживается Evrone.
Этот шаблон поддерживает три типа серверов:
- AMQP RPC (на основе RabbitMQ в качестве транспорта и Request-Reply паттерна)
- NATS RPC (на основе NATS в качестве транспорта и Request-Reply паттерна))
- gRPC (gRPC фреймворк на основе protobuf)
- REST API (Fiber фреймворк)
# Postgres, RabbitMQ, NATS
make compose-up
# Запуск приложения и миграций
make run# DB, app + migrations, integration tests
make compose-up-integration-testmake compose-up-all Проверьте сервисы:
- AMQP RPC:
- URL:
amqp://guest:guest@127.0.0.1:5672/ - Client Exchange:
rpc_client - Server Exchange:
rpc_server
- URL:
- NATS RPC:
- URL:
nats://guest:guest@127.0.0.1:4222/ - Server Exchange:
rpc_server
- URL:
- REST API:
- gRPC:
- URL:
tcp://grpc.lvh.me:8081|tcp://127.0.0.1:8081 - v1/translation.history.proto
- URL:
- PostgreSQL:
postgres://user:myAwEsOm3pa55@w0rd@127.0.0.1:5432/db
- RabbitMQ:
- http://rabbitmq.lvh.me | http://127.0.0.1:15672
- Credentials:
guest/guest
- NATS monitoring:
- http://nats.lvh.me | http://127.0.0.1:8222/
- Credentials:
guest/guest
Инициализация конфигурации и логгера. Здесь вызывается основная часть приложения из internal/app/app.go.
Приложение двенадцати факторов хранит конфигурацию в переменных окружения (часто сокращается до env vars или env).
Переменные окружения легко изменить между развёртываниями, не изменяя код; в отличие от файлов конфигурации, менее
вероятно случайно сохранить их в репозиторий кода; и в отличие от пользовательских конфигурационных файлов или других
механизмов конфигурации, таких как Java System Properties, они являются независимым от языка и операционной системы
стандартом.
Конфигурация: config.go
Пример: .env.example
docker-compose.yml использует переменные env для настройки сервисов.
Документация Swagger. Генерируется автоматически с помощью библиотеки swag. Вам не нужно ничего редактировать вручную.
Protobuf файлы. Они используются для генерации Go-кода для gRPC сервисов. Protobuf файлы также используются для генерации документации для gRPC сервисов. Вам не нужно ничего исправлять самостоятельно.
Интеграционные тесты. Они запускаются в отдельном контейнере, рядом с контейнером приложения.
Здесь находится только одна функция Run. Она размещена в файле app.go и является логическим продолжением функции
main.
Здесь создаются все основные объекты. Внедрение зависимостей происходит через конструктор "New ...". Это позволяет слоировать приложение, делая бизнес-логику независимой от других слоев.
Далее запускается сервер и ожидается сигнал в select для корректного завершения работы.
Если app.go стал слишком большим, вы можете разделить его на несколько файлов.
Если зависимостей много, то для удобства можно использовать wire.
Файл migrate.go используется для автоматической миграции базы данных.
Он включается в компиляцию только при указании тега migrate.
Пример:
go run -tags migrate ./cmd/appСлой хэндлеров сервера (MVC контроллеры). В шаблоне показана работа 3 серверов:
- AMQP RPC (на основе RabbitMQ в качестве транспорта)
- gRPC (gRPC фреймворк на основе protobuf)
- REST API (Fiber фреймворк)
Маршрутизаторы http сервера пишутся в едином стиле:
- Хэндлеры группируются по области применения (по общему критерию)
- Для каждой группы создается свой маршрутизатор
- Объект бизнес-логики передается в маршрутизатор, чтобы быть доступным внутри хэндлеров
Простое версионирование RPC.
Для версии v2 нужно будет добавить папку amqp_rpc/v2 с таким же содержимым.
А в файле internal/controller/amqp_rpc/router.go добавить строку:
routes := make(map[string]server.CallHandler)
{
v1.NewTranslationRoutes(routes, t, l)
}
{
v2.NewTranslationRoutes(routes, t, l)
}Простое версионирование gRPC.
Для версии v2 нужно будет добавить папку grpc/v2 с таким же содержимым.
Также добавьте папку v2 в proto-файлы в docs/proto.
И в файле internal/controller/grpc/router.go добавьте строку:
{
v1.NewTranslationRoutes(app, t, l)
}
{
v2.NewTranslationRoutes(app, t, l)
}
reflection.Register(app)Простое версионирование RPC.
Для версии v2 нужно будет добавить папку nats_rpc/v2 с таким же содержимым.
А в файле internal/controller/nats_rpc/router.go добавить строку:
routes := make(map[string]server.CallHandler)
{
v1.NewTranslationRoutes(routes, t, l)
}
{
v2.NewTranslationRoutes(routes, t, l)
}Простое версионирование REST API.
Для создания версии v2 нужно создать папку restapi/v2 с таким же содержимым.
Добавить в файл internal/controller/restapi/router.go строки:
apiV1Group := app.Group("/v1")
{
v1.NewTranslationRoutes(apiV1Group, t, l)
}
apiV2Group := app.Group("/v2")
{
v2.NewTranslationRoutes(apiV2Group, t, l)
}Вместо Fiber можно использовать любой другой http фреймворк.
В файле router.go над хэндлером написаны комментарии для генерации документации через
swagger swag.
Сущности бизнес-логики (модели). Могут быть использованы в любом слое. Также они могут иметь методы, например, для валидации.
Бизнес-логика.
- Методы группируются по области применения (по общему критерию)
- У каждой группы своя отдельная структура
- Один файл - одна структура
Репозитории, webapi, rpc и другие структуры передаются в слой бизнес-логики в связующем файле internal/app/app.go
(смотрите Внедрение зависимостей).
Репозиторий — это абстрактное хранилище (база данных), с которым взаимодействует бизнес-логика.
Это абстрактное web API, с которым взаимодействует бизнес-логика. Например, это может быть внешний микросервис, к которому бизнес-логика обращается через REST API. Название пакета выбирается таким, чтобы соответствовать его назначению.
RabbitMQ RPC паттерн:
- Внутри RabbitMQ не используется маршрутизация
- Используется fanout-обмен, к которому привязана одна эксклюзивная очередь - это наиболее производительная конфигурация
- Переподключение при потере соединения
Для устранения зависимости бизнес-логики от внешних пакетов используется внедрение зависимостей.
Например, через конструктор "New" внедряется репозиторий в слой бизнес-логики.
Это делает бизнес-логику независимой и переносимой.
Мы можем переписать реализацию интерфейса репозитория, не внося изменения в пакет бизнес-логики usecase.
package usecase
import (
// Nothing!
)
type Repository interface {
Get()
}
type UseCase struct {
repo Repository
}
func New(r Repository) *UseCase {
return &UseCase{
repo: r,
}
}
func (uc *UseCase) Do() {
uc.repo.Get()
}Благодаря разделению через интерфейсы можно генерировать моки (например, используя mockery) и легко писать юнит-тесты.
Мы не привязаны к конкретным реализациям и всегда можем заменить один компонент на другой. Если новый компонент реализует интерфейс, то в бизнес-логике ничего не нужно менять.
Программисты создают оптимальную архитектуру приложения после написания основной части кода.
Хорошая архитектура позволяет откладывать изменения как можно дольше.
Инверсия зависимостей (та же, что и в SOLID) используется как принцип для внедрения зависимостей. Зависимости направлены от внешнего слоя к внутреннему. Благодаря этому бизнес-логика и сущности остаются независимыми от других частей системы.
Например, приложение можно разделить на два слоя - внутренний и внешний:
- Бизнес-логика (например, стандартная библиотека Go).
- Инструменты (базы данных, серверы, брокеры сообщений и другие библиотеки и фреймворки).
Внутренний слой с бизнес-логикой должен быть чистым. Он обязан:
- Не импортировать пакеты из внешних слоев.
- Использовать только стандартную библиотеку.
- Взаимодействовать с внешними слоями через интерфейсы (!).
Бизнес-логика не должна ничего знать о Postgres или о реализации web API. Бизнес-логика имеет интерфейс для взаимодействия с абстрактной базой данных или абстрактным web API.
Внешний слой имеет ограничения:
- Компоненты этого слоя не могут знать друг о друге и взаимодействовать напрямую. Обращение друг к другу происходит через внутренний слой - слой бизнес-логики.
- Вызовы во внутренний слой выполняются через интерфейсы (!).
- Данные передаются в формате, удобном для бизнес-логики (структуры хранятся в
internal/entity).
Например, нужно обратиться к базе данных из HTTP хэндлера (в слое контроллер).
База данных и HTTP находятся во внешнем слое. Они не знают друг о друге ничего и не могут взаимодействовать напрямую.
Взаимодействие будет происходить через слой бизнес-логики usecase:
HTTP > usecase
usecase > repository (Postgres)
usecase < repository (Postgres)
HTTP < usecase
Символы > и < показывают пересечения слоев через интерфейсы и направления. Это же показано на схеме:
Пример более сложного пути данных:
HTTP > usecase
usecase > repository
usecase < repository
usecase > webapi
usecase < webapi
usecase > RPC
usecase < RPC
usecase > repository
usecase < repository
HTTP < usecase
- Entities (сущности) - это структуры, с которыми работает бизнес-логика.
Они располагаются в папке
internal/entity. В терминологии MVC сущности - это модели. - Use Cases - это бизнес-логика. Располагается в папке
internal/usecase.
Слой, с которым бизнес-логика взаимодействует напрямую, обычно называется инфраструктурным слоем.
Это может быть репозиторий internal/usecase/repo, внешнее webapi internal/usecase/webapi, любой пакет или
микросервис.
В шаблоне пакеты infrastructure размещены внутри internal/usecase.
Вы можете выбирать, как называть точки входа, по своему усмотрению. Варианты такие:
- controller (в нашем случае)
- delivery
- transport
- gateways
- entrypoints
- primary
- input
В классической версии Чистой Архитектуры для создания больших монолитных приложений предложено 4 слоя.
В исходной версии внешний слой делится на два, которые также имеют инверсию зависимостей в другие слои и взаимодействуют через интерфейсы.
Внутренний слой также делится на два (с использованием интерфейсов) в случае сложной логики.
Сложные инструменты могут быть разделены на дополнительные слои. Однако добавлять слои следует только в том случае, если это действительно необходимо.
Кроме Чистой Архитектуры есть и другие подходы:
- Луковая Архитектура
- Гексагональная (Порты и адаптеры также похожа на неё) Они обе основаны на принципе инверсии зависимостей. Порты и адаптеры очень похожи на Чистую Архитектуру. Различия в основном заключаются в терминологии.


