Архитектура Go-сервиса
Пока сервис помещается в один файл, об архитектуре можно не думать — обработчик сам ходит в БД, сам собирает JSON, сам считает бизнес-правила. Архитектура начинается там, где этой простоты больше нет: сервис растёт, появляется второй транспорт, меняется внешний API, хранилище хочется подменить в тесте. Здесь решает не объём кода, а границы между его частями и направление зависимостей между этими границами.
Главная ловушка темы — направить зависимость наружу: дать доменному слою импортировать драйвер БД, навесить на одну struct JSON-теги, db-теги и бизнес-методы, закрыть клиент БД раньше HTTP-сервера. Каждое такое решение связывает то, что должно быть развязано, и расплата приходит позже — когда хранилище не заменить, переименование поля в JSON ломает бизнес-логику, а остановка сервиса роняет запросы на уже мёртвом соединении. Эта тема разбирает архитектуру по слоям — от правила зависимостей до корректного завершения процесса.
Карта темы
- Слоистая архитектура — разделение на транспорт, домен и хранилище и правило зависимостей, направленных внутрь.
- Чистая архитектура — концентрические кольца и правило зависимостей, направленных только внутрь, через инверсию зависимостей.
- Паттерн adapter — стабильный внутренний интерфейс, за которым прячется изменчивый внешний API или формат.
- DTO против доменной сущности — транспортный контейнер данных против носителя бизнес-состояния, инвариантов и поведения.
- Graceful shutdown — завершение компонентов в обратном порядке зависимостей с дренажом in-flight запросов.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Направлять зависимости наружу — домен импортирует драйвер БД | Бизнес-правила привязаны к конкретной СУБД, край не заменить |
| Путать слои с отдельно развёртываемыми сервисами | Неверная модель — слои живут внутри одного процесса, граница логическая |
| Растащить бизнес-логику по транспортным обработчикам | Доменный слой становится анемичным, от слоистости остаётся одно название |
| Считать, что зависимость в чистой архитектуре идёт туда же, куда вызов | Поток вызова — наружу, зависимость в коде — внутрь; стрелку разворачивает интерфейс |
Думать, что скопировать внешнюю struct к себе уже изолирует | Копия повторяет чужую форму — при смене формата правишь её и всех вызывающих |
| Помещать adapter в доменный слой | Внешняя зависимость пробирается внутрь и заражает бизнес-правила изменчивым API |
Переиспользовать одну struct как DTO и доменную сущность | JSON-теги, db-теги и форма API текут в бизнес-логику — три причины менять один тип |
| Навешивать доменные инварианты на DTO | DTO пересоздаётся на каждый запрос и не источник истины — инвариант на нём ничего не охраняет |
| Закрывать клиент БД раньше HTTP-сервера при остановке | Дренируемые запросы бьются о мёртвое соединение — пользователь видит 500 |
| Останавливать сервис без таймаута на дренаж | Shutdown может зависнуть навсегда, если запросы не завершаются |
Значение для собеседований
Архитектура — обязательная тема на senior-уровне Go-интервью, и спрашивают не схему на доске, а понимание границ и направления зависимостей. Интервьюер проверяет, отделяете ли вы бизнес-правила от деталей — транспорта, хранилища, внешних API — или сваливаете всё в один слой.
Что обычно проверяют:
- Правило слоистой архитектуры — зависимости направлены внутрь, доменный слой не импортирует ни транспорт, ни БД.
- Чем слой отличается от сервиса — слой это граница в коде, сервис граница развёртывания; все слои в одном бинарнике.
- Как инверсия зависимостей разворачивает стрелку — интерфейс объявляет потребитель, реализацию подставляют снаружи.
- Зачем нужен adapter — поглотить изменение внешнего API в одном месте за стабильным интерфейсом, и почему ему не место в домене.
- Почему DTO и доменная сущность — разные типы, и что протекает, если объединить их в один.
- Почему остановка идёт в обратном порядке зависимостей и зачем дренаж in-flight запросов под
contextс таймаутом.
Типичный неверный ответ: «домен может сам сходить в базу, это же быстрее». Это запускает разбор того, что прямой импорт драйвера БД в домене прибивает бизнес-правила к конкретной СУБД, ломает тестируемость и делает хранилище незаменяемым — а правильная граница держится на интерфейсе, который объявляет домен, а реализует adapter снаружи.