Прелюдия
Важность хорошей структуризации проекта могут недооценивать только совсем уж начинающие программисты. Сопровождение и развитие проекта с непродуманной структурой кода со временем превращается в сущий ад. Тут выплывают и проблемы со связанностью модулей и проблемы с плохой читабельностью и экспонтециально растущая трудоемкость развития проекта. Но всякая структура должна подчиняться некой логической концепции. И тут появляется великое поле для холиваров.
Всех проблем, вызванных изначально непродуманной структурой чужих и своих проектов я так наелся за годы работы программистом, что у меня появился некий панический страх перед этим. На начальном этапе работы над любым сколько-нибудь большим проектом я мог неоправданно часто с нуля переписывать всё, если обнаруживал, что структура проекта в чем-то неоптимальна.
Проблема в том, что на мой взгляд, в любом языке программирования не существует какой-то идеальной концепции структуры проекта, подходящей для большинства задач. И в конечном итоге, оптимальным решением, как это часто бывает в жизни, является некий компромисс.
С небес на землю
Собственно, что касается конкретно проектов разрабатываемых на языке Go, есть достаточное количество концепций, шаблонов архитектур. При этом, возникает не только вопрос выбора, но и немаловажный вопрос - где провести черту компромисса? До какой степени следовать выбранной концепции? Например, много всего написано про "чистую" архитектуру, но всегда ли оправдано слепое следование всем её правилам при разработке небольшого микросервиса? В этом месте, я ожидаю камней в мою сторону, но тем не менее, мой ответ - нет. И это вполне подтверждается боевым опытом.
Ближе к телу
Я перешел в Go разработку, имея хороший опыт разработки в Delphi, и небольшой - разработки приложений под Android. Поэтому, вопрос выбора архитектуры отнюдь не считал праздным. Ну и следуя принципу "не изобретай то, что изобретено до тебя", полез читать статьи, смотреть примеры... Потратив на это некое приличное волне время, обнаружил, что ясности в голове сильно больше не стало. Да везде встречаю слои, везде упоминается некая "чистая" архитектура, но и всё. Дальше - каждый молится своему богу. Разбивают на слои по-разному, изолируют слои по разному, называют сущности по-разному. И при этом мою голову сверлила одна и та же мысль - насколько оправдано "городить огород" следуя прекрасной идеалистической концепции, когда мы пишем небольшой микросервис? Можно ведь так дойти до абсурда, когда 90% кода обеспечивают бескомпромиссную архитектуру, а функциональность - оставшиеся 10%. И главное, поддерживать потом всё это будет сложнее, а не проще. Как говорится - за что боролись, на то и напоролись. Ну взять хотя бы пресловутую изоляцию слоёв - так ли всегда оправданы отдельные модели для каждого слоя для одной и той же сущности в микросервисе?
Предмет разговора
Во многих шаблонах, которые я смотрел, пытаясь выбрать удобный для себя, меня смущало отсутствие однозначной логики в разбивке на слои. Хотелось видеть максимально понятный и простой принцип, по которому код разбит на слои. Чтоб не ломать потом голову - куда правильнее поместить новую структуру (модуль) и не кусать локти, когда в итоге она оказалась не в том месте. Вероятно, я не достаточно вчитывался и проникался... Тем не менее, в конечном итоге, я изобрел свой велосипед, хотя и являюсь противником велосипедостроения.
Собственно слои нас в первую очередь избавляют от перекрестной связанности между модулями, что является самым большим (наверное) злом в любой архитектуре.
Принцип, которым я руководствовался - "клиент/сервер" в максимально абстрактном понимании. Клиент - тот кто потребляет некую услугу, Сервер - тот кто предоставляет эту услугу. Всё. Всего слоев - три. Таким образом, первый слой моего шаблона - это некие внешние "ручки" (REST API. Брокер очередей и т. п.). Этот слой носит название "api" (просьба сильно не бить ногами за нейминг тут и далее, он спорный, но не имеет принципиального значения). Слой api является клиентом по отношению к нижележащему слою бизнес логики, именуемому "service". Ну тут, думается, можно обойтись без пояснений - бизнес-логика, она бизнес-логика и есть. Слой "service" в свою очередь является клиентом по отношению к слою "controller". Слой "controller", это слой низкоуровневых тупых исполнителей, максимально лишенный логики - repository, отправка любых запросов наружу и пр. Собственно, слово "controller" взято из лексикона компьютерной техники - контроллер дисковода, контроллер дисплея. И да, термин спорный. Таким образом, все вызовы между слоями распространяются сверху вниз, в одну сторону - от клиента к серверу. Это главное и это гарантировано избавляет от перекрестной связанности. И логика тут максимально проста и понятна. Собственно, это и всё, в основном.
Некоторые пояснения
В слое api помимо вызовов, приходящих извне должны, следуя логике, располагаться и любые внутренние клиенты, генерирующие вызовы к слою service, например планировщик заданий. Если какой-то модуль может играть роль и клиента и сервера, его следует разбить на два интерфейса, даже если под ними будет лежать один объект (лучше не использовать callback для перекрестных вызовов). Пример - модуль брокера очередей, который может и отправлять сообщения и реагировать на входящие сообщения. Создаем один объект, реализующий два интерфейса и используем его как два разных интерфейса в слоях api и controller. Как вариант, если такая необходимость появляется.
Прочие плюшки
Вопреки принципам "чистой архитектуры", по возможностью я использую одну модель для одной сущности во всех слоях. Общие модели, используемые в разных слоях складываю в пакет internal/model (не внутрь одного из слоёв!). Мой опыт показывает, что адаптация модели под работу одновременно и с БД и с HTTP API (struct tags, marshalling/unmarshalling) - всё-таки меньшее зло, чем бессмысленное в плане функциональности копирование и переупаковка одних и тех же данных из объекта в объект. Но это не всегда возможно, особенно если вы используете кодогенерацию Design-First, когда структуры создаются автоматически. Кодогенерация Code-First при реализации API избавляет от этой проблемы и многих других, в частности с ней количество кода в слое API получается в разы меньше и он более гибок и понятен.
Внутри слоев я стараюсь единообразно разбивать код на файлы/пакеты если это возможно. К примеру, если в слое api есть два файла user.go goods.go, то и в слое service есть одноименные файлы и в пакете repository слоя controller - ровно те же user.go goods.go. Это мелочь, но она существенно снижает когнитивную нагрузку при работе с проектом.
Послесловие
Я не пытаюсь навязать кому-то свой подход как лучший из лучших. Просто хочу поделиться опытом. Опыт вполне себе успешный - больше десятка сервисов. Конструктивных недостатков у данной концепции я пока не обнаружил. В любом случае буду благодарен за аргументированную критику и советы.
vkomp
Не очень понятно, что вы предлагаете. В принципе все делят на слои, а как - это уже "все мужчины имеют свой способ избавления от последней капли".
Понятно выделение слоя данных, а вот с бизнес-логикой не все понятно - где границы связности сущностей?! База в том, что сущности без связности вообще не работают. А если связывать в логике, то голанг видит зацикленность импортов. Кто-то разбивает через интерфейсы, это весьма тяжело поддерживать, когда много сущностей связано с многими сущностями.
При этом не вижу ценности делать "тупые контроллеры" - я обнаружил, что этот слой очень ценен, так как обладает особенной логикой (https://habr.com/ru/articles/901938/). Например: http-обработчик проверяет доступ через jwt-токен, и выдает http-ошибку, а обработчик из очереди не будет проверять доступ перед исполнением, и выдает ошибку только в логгер.
И такой подход юзаю уже год - код легко поддерживать, изменения вносить очень удобно. Пока кризиса не предвижу.