Архитектура Go-сервиса
Слоистая и чистая архитектура, направление зависимостей, паттерн adapter, DTO против доменной сущности и graceful shutdown.
7 вопросов
JuniorТеорияЧастоЧто такое слоистая архитектура и зачем разделять транспорт, домен и хранилище?
Что такое слоистая архитектура и зачем разделять транспорт, домен и хранилище?
Приложение делится на слой транспорта (обработчики HTTP/gRPC), доменный слой (бизнес-логика) и слой хранилища (репозитории), а зависимости направлены внутрь. Доменный слой не импортирует транспорт или БД, поэтому любой край можно заменить, не трогая бизнес-правила.
Типичные ошибки
- ✗Направлять зависимости наружу, из-за чего домен импортирует драйвер БД и бизнес-правила привязываются к хранилищу
- ✗Путать слои с отдельно развёртываемыми сервисами — слои живут внутри одного процесса
- ✗Помещать бизнес-логику в обработчик транспорта, оставляя доменный слой анемичным
Уточняющие вопросы
- →Куда вы поместите валидацию ввода — в транспорт или домен — и почему?
- →Как доменный слой обращается к хранилищу, не импортируя пакет БД?
MiddleТеорияЧастоКак архитектурный подход Clean Architecture обеспечивает своё правило зависимостей?
Как архитектурный подход Clean Architecture обеспечивает своё правило зависимостей?
Clean Architecture располагает код концентрическими слоями — сущности, сценарии, адаптеры, фреймворки — по одному правилу: зависимости в исходном коде направлены только внутрь. Доменный слой не импортирует код БД, HTTP или фреймворков; внешние детали реализуют интерфейсы, заданные внутренними слоями, поэтому фреймворки становятся заменяемыми плагинами.
Типичные ошибки
- ✗Путать Clean Architecture с горизонтальным трёхуровневым делением — её слои концентрические с правилом зависимости внутрь
- ✗Позволять домену импортировать пакет БД или фреймворка напрямую вместо зависимости от интерфейса
- ✗Думать, что правило зависимостей обеспечивается именами папок, а не тем, какой пакет какой импортирует
Уточняющие вопросы
- →Как инверсия зависимостей позволяет домену задавать интерфейс репозитория, реализуемый внешним слоем?
- →Где располагаются DTO относительно сценариев и сущностей в Clean Architecture?
MiddleДизайнЧастоGo-сервис в main связывает три компонента: клиент БД, доменный сервис, выполняющий фоновую работу с этой БД, и HTTP-сервер, чьи обработчики вызывают доменный сервис. По SIGINT/SIGTERM нужно корректно всё завершить. В каком порядке закрывать три компонента и почему порядок важен? Опишите, как main ждёт сигнала завершения и что станет с запросами «в полёте», если закрыть БД раньше HTTP-сервера.
Go-сервис в main связывает три компонента: клиент БД, доменный сервис, выполняющий фоновую работу с этой БД, и HTTP-сервер, чьи обработчики вызывают доменный сервис. По SIGINT/SIGTERM нужно корректно всё завершить. В каком порядке закрывать три компонента и почему порядок важен? Опишите, как main ждёт сигнала завершения и что станет с запросами «в полёте», если закрыть БД раньше HTTP-сервера.
Закрывайте в обратном порядке зависимостей: сперва HTTP-сервер (перестать принимать новые запросы и дренировать текущие), затем доменный сервис (доделать фоновую работу), и БД последней. main блокируется на channel сигналов, заведённом через signal.Notify(c, SIGINT, SIGTERM), и продолжает по приёму. Если закрыть БД первой, обработчики «в полёте» и фоновая работа внезапно упрутся в мёртвое соединение и вернут клиентам ошибки — противоположность корректному завершению.
Типичные ошибки
- ✗Закрывать БД первой, ломая обработчики «в полёте» и фоновую работу
- ✗Думать, что порядок неважен, и закрывать все компоненты конкурентно
- ✗Считать, что закрытие БД ставит запросы в очередь, а не проваливает их
Уточняющие вопросы
- →Как
http.Server.Shutdownдренирует запросы «в полёте» перед возвратом? - →Почему добавить таймаут на всё завершение, чтобы зависший дренаж не висел вечно?
MiddleТеорияИногдаКак adapter или facade изолирует код от внешнего формата данных?
Как adapter или facade изолирует код от внешнего формата данных?
Вы определяете стабильный внутренний интерфейс, от которого зависит ваш код, затем пишете adapter, который оборачивает внешний API или формат и реализует этот интерфейс. При изменении внешней стороны переписывается только adapter — вызывающий код не трогается, ведь он видит лишь внутренний интерфейс.
Типичные ошибки
- ✗Позволять вызывающим зависеть от типов внешней библиотеки напрямую, а не от внутреннего интерфейса
- ✗Думать, что копирование внешней структуры в свой пакет изолирует — она всё равно меняется вслед за ними
- ✗Помещать логику adapter в доменный слой, а не на край хранилища или транспорта
Уточняющие вопросы
- →Где располагается adapter относительно доменного слоя?
- →Как тестировать доменный код без реальной внешней зависимости?
MiddleТеорияИногдаВ чём разница между DTO и доменной сущностью в слоистом приложении?
В чём разница между DTO и доменной сущностью в слоистом приложении?
DTO — это транспортно-ориентированный контейнер данных: простые поля, без поведения, нужный для (де)сериализации запросов и ответов. Доменная сущность держит бизнес-состояние плюс инварианты и методы. Их разделение не даёт JSON-тегам и форме API протечь в бизнес-правила.
Типичные ошибки
- ✗Переиспользовать одну структуру как DTO и сущность, из-за чего JSON-теги и имена полей API текут в бизнес-логику
- ✗Считать, что DTO несёт поведение — это только данные, методы и инварианты держит сущность
- ✗Думать, что DTO и сущность обязаны иметь одинаковые поля — DTO повторяет форму канала, а не домен
Уточняющие вопросы
- →Где в слоистом приложении происходит маппинг DTO в сущность?
- →Почему доменные инварианты не должны жить на DTO?
SeniorДизайнИногдаСпроектируйте систему фиче-флагов для парка из многих инстансов Go-сервисов. Операторы переключают флаги (вкл/выкл, процентные выкатки, правила на сегмент) из центральной панели, и каждый инстанс вычисляет флаги, чтобы выбрать поведение. Требования:
- Вычисление — на горячем пути почти каждого запроса, поэтому проверка обязана быть дешёвой и не делать синхронный сетевой вызов на запрос.
- Когда флаг меняется, новое значение быстро распространяется на все инстансы, но мгновенное переключение всего парка не требуется — определите, какую согласованность вы реально даёте во время изменения.
- Один пользователь не должен мерцать между старым и новым поведением, пока инстансы ещё сходятся.
- Сервис флагов — источник истины, но вычисление обязано продолжать работать с безопасным дефолтом, если он на короткое время недоступен.
Опишите, где происходит вычисление, как обновления доходят до каждого инстанса и какая гарантия согласованности держится по парку в момент изменения.
Спроектируйте систему фиче-флагов для парка из многих инстансов Go-сервисов. Операторы переключают флаги (вкл/выкл, процентные выкатки, правила на сегмент) из центральной панели, и каждый инстанс вычисляет флаги, чтобы выбрать поведение. Требования: - Вычисление — на горячем пути почти каждого запроса, поэтому проверка обязана быть дешёвой и не делать синхронный сетевой вызов на запрос. - Когда флаг меняется, новое значение быстро распространяется на все инстансы, но мгновенное переключение всего парка не требуется — определите, какую согласованность вы реально даёте во время изменения. - Один пользователь не должен мерцать между старым и новым поведением, пока инстансы ещё сходятся. - Сервис флагов — источник истины, но вычисление обязано продолжать работать с безопасным дефолтом, если он на короткое время недоступен. Опишите, где происходит вычисление, как обновления доходят до каждого инстанса и какая гарантия согласованности держится по парку в момент изменения.
Вычисляйте флаги локально по снимку в процессе, чтобы проверка была дешёвым lookup по map без сетевого вызова на горячем пути. Сервис флагов — источник истины; каждый инстанс стримит обновления (или опрашивает с ETag) и атомарно подменяет снимок. Во время изменения ждите краткой итоговой согласованности по парку — делайте правила детерминированными на пользователя, чтобы он видел один стабильный ответ, пока инстансы сходятся.
Типичные ошибки
- ✗Звать сервис флагов на каждом запросе, добавляя сетевой хоп и жёсткую зависимость на каждый горячий путь
- ✗Ждать сильно согласованного переключения по всему парку, тогда как стримленные снимки итогово согласованы
- ✗Вычислять случайно на запрос, а не детерминированно на пользователя, из-за чего пользователь мерцает между вариантами
Уточняющие вопросы
- →Как удержать одного пользователя на одном варианте, хотя инстансы обновляют снимки в разное время?
- →Что будет с вычислением, если сервис флагов недоступен, и каков безопасный дефолт?
SeniorДизайнИногдаСпроектируйте публичный Go-API пакета-клиента для сетевого key-value хранилища (вроде memcached). Набросайте только экспортируемые типы и сигнатуры функций и методов — без реализации. Удовлетворите требованиям:
- Инициализация принимает один обязательный параметр (addr string, напр. 10.11.0.12:6379) и опциональные (authKey string, connTimeout time.Duration). Добавление нового опционального параметра позже не должно ломать существующих вызывающих.
- Избегайте двухэтапного конструктора (Create, затем Init/Connect) и отдельного конструктора под каждую комбинацию опциональных параметров.
- Read и Write должны принимать context.Context, чтобы вызывающие задавали дедлайны и несли трассировку.
- Read должен различать «ключа нет» и «ключ есть, но значение nil/пустое» — решите, как это выражает сигнатура.
Объясните выбор обработки опциональных параметров, почему context.Context — первый аргумент, и как Read снимает неоднозначность «нет ключа» против «пусто».
Спроектируйте публичный Go-API пакета-клиента для сетевого key-value хранилища (вроде memcached). Набросайте только экспортируемые типы и сигнатуры функций и методов — без реализации. Удовлетворите требованиям:
- Инициализация принимает один обязательный параметр (addr string, напр. 10.11.0.12:6379) и опциональные (authKey string, connTimeout time.Duration). Добавление нового опционального параметра позже не должно ломать существующих вызывающих.
- Избегайте двухэтапного конструктора (Create, затем Init/Connect) и отдельного конструктора под каждую комбинацию опциональных параметров.
- Read и Write должны принимать context.Context, чтобы вызывающие задавали дедлайны и несли трассировку.
- Read должен различать «ключа нет» и «ключ есть, но значение nil/пустое» — решите, как это выражает сигнатура.
Объясните выбор обработки опциональных параметров, почему context.Context — первый аргумент, и как Read снимает неоднозначность «нет ключа» против «пусто».
Паттерн функциональных опций: New(addr string, opts ...Option) (*Client, error) с опциями WithAuthKey/WithTimeout — обязательный параметр позиционный, опциональные расширяемы без поломки вызывающих, один конструктор. Методы первым берут context.Context по соглашению, чтобы дедлайны/трассировка проходили внутрь. Неоднозначность nil/нет ключа решают явным сигналом: Read(ctx, key) ([]byte, bool, error) (флаг found) или sentinel ErrNotFound.
Типичные ошибки
- ✗Двухэтапный Create+Init или конструктор на комбинацию опций вместо опций
- ✗Передавать сырой
time.Durationдедлайн вместоcontext.Context - ✗Возвращать
nilи для отсутствия, и для пустого, не различая случаи
Уточняющие вопросы
- →Как функциональные опции сохраняют обратную совместимость при новой настройке?
- →Почему в части API sentinel
ErrNotFoundпредпочтительнее флагаfound bool?