Всем привет, меня зовут Авксентий, я backend-разработчик в inDriver. Думаю, каждый начинающий разработчик сталкивался с проблемой, как правильно выстроить архитектуру и структуру проекта. Ведь организация кода проекта — постоянно развивающаяся проблема, а следование стандартной структуре сохраняет чистоту кода и повышает производительность команды.
Когда я начинал писать на Go, то потратил много времени на поиски стандартов структурирования проекта. В итоге так и не нашел официального и точного стандарта — либо информация была неполной, либо это было не то, что нужно. Я решил написать свой гайд на основе опыта. Он для начинающих разработчиков и посвящен тому, как структурировать проект на Golang.
Почему я решил написать эту статью
Правильно выстроенные архитектура и структура проекта в начале способствует безболезненной разработке, масштабированию и легкому внедрению новых разработчиков в проект. Конечно, плоскую структуру тоже можно использовать для маленьких проектов с одним main-файлом, но это непрактично для больших.
Я видел немало проектов, и из каждого взял что-то для себя. Рекомендую ознакомиться с этим проектом — многие ориентируется на него при создании структуры. В моем примере будет более компактная структура — мы детально разберем директорию /internal.
Конечно, метод не претендует на лучший или единственный способ ведения дел. Но, думаю, он хорошо подойдет для начала. Посмотрим на структуру корня проекта:
Директории
/cmd
Точка входа для нашего приложения. Имя директории для каждого приложения должно совпадать с именем исполняемого файла, который вы хотите собрать. Не стоит располагать в этой директории много кода. Самой распространенной практикой является использование маленькой main-функции, которая импортирует и вызывает весь необходимый код из директорий /internal и /pkg.
/internal
Сердце нашего приложения — всю внутреннюю логику приложения храним здесь. /internal не импортируем в других приложениях и библиотеках. Код, который написан тут, предназначен исключительно для внутреннего использования в рамках кодовой базы. С версии Go 1.4 определен механизм, который не позволяет импортировать пакеты вне данного проекта, если они находятся внутри /internal.
В /internal мы храним бизнес-логику проекта и работу с базами данными. В общем, всю логику, связанную с этим приложением. Выстроить структуру внутри /internal можно по-разному, в зависимости от архитектуры. Но я не буду сильно углубляться в нее, а поверхностно покажу, как это выглядит. Приведу пример трехуровневой архитектуры, когда приложение делится на 3 слоя:
Транспортный.
Бизнес.
Базы данных.
Логика должна быть выстроена так, чтобы слои иерархически обращались друг к другу сверху вниз и наоборот. Не допускается, чтобы один слой перескакивал через промежуточный (например, транспортный напрямую в базу данных) и нижний не общался с верхним (например, база данных ходит в транспортный слой).
Транспортный слой:
Сетевой уровень приложения, на котором конечный пользователь взаимодействует с приложением. После обработки запроса вся собранная информация идет на слой ниже.
Бизнес-слой:
Как следует из названия, содержит бизнес-логику, которая поддерживает основные функции приложения. Если в логике затрагиваются базы данных, мы идем на слой ниже.
Слой базы данных:
Отвечает за взаимодействие с постоянными хранилищами, такими как базы данных, и прочую обработку информации, которая не связана с бизнесом. Например, чтение и запись в базе данных.
Директории /internal:
/app
Точка, где все наши зависимости и логика собираются и запускают приложение. Run-метод, который вызывается из /cmd./config
Инициализация общих конфигураций приложения, которые мы прописывали в корне проекта./database (слой базы данных)
Файлы содержат методы для взаимодействия с базами данных./models (слой базы данных)
Структуры таблиц баз данных./services (бизнес-слой)
Вся бизнес-логика приложения./transport (транспортный слой)
Здесь храним http-настройки сервера, хендлеры, порты и так далее.
/pkg
Если в /internal мы хранили код, который не могли импортировать в других приложениях, то в /pkg храним библиотеки, используемые в сторонних приложениях. Это нужно, чтобы потом импортировать их в другой проект, а не дублировать код из проекта в проект. В общем, кастомные или общие библиотеки мы храним здесь.
Вы можете не использовать эту директорию, если проект совсем небольшой и добавление нового уровня вложенности не имеет практического смысла.
/configs
Статические конфигурации нашего приложения, связанные с процессом сборки приложения. Обычно это yaml-файлы.
/api
Документация по вашему API. Спецификации OpenAPI или Swagger, файлы JSON Schema, файлы определения протоколов.
/build
Файлы конфигурации для билда проекта, Docker-контейнера и так далее.
/deployments
Содержит файлы, связанные с развертыванием: плейбуки Ansible, манифесты Docker Compose, манифесты и настройки Kuberntes, диаграммы Helm.
/docs
Документирование кода — важная часть в начале проекта. Поэтому всю документацию кода и дизайна (в дополнение к автоматической документации Godoc) храним здесь.
README.md
Трудно ожидать, что кто-то захочет погрузиться в ваш код, если ему не предоставили общего описания проекта. Поэтому файл README тоже необходим.
Распространенные директории
Хочу показать распространенные директории, которые я не включил в свой проект. Вы можете ознакомиться с ними, и в случае необходимости добавить себе.
/scripts
Скрипты для сборки, установки, анализа и прочих операций над проектом. Они позволяют оставить основной Makefile небольшим и простым.
/testdata
Дополнительные внешние приложения и данные для тестирования. Вы можете организовывать структуру директории /test так, как вам угодно. Для больших проектов имеет смысл создавать вложенную директорию с данными для тестов.
/tools
Инструменты поддержки проекта. Отмечу, что эти инструменты могут импортировать код из директорий /pkg и /internal.
/assets
Другие ресурсы, необходимые для работы: например, картинки и логотипы.
/web
Эта директория понадобится, если вы реализуете веб-приложение. Здесь находятся специальные компоненты для веб-приложений: статические веб-ресурсы, серверные шаблоны и одностраничные приложения.
/migrations
Здесь все миграции, связанные с базами данными: например, SQL-файлы.
Вывод
Конечно, не надо строго следовать моей структуре. Можно взять часть и отредактировать все под себя. Но когда я начинал, мне не хватало такого подробного гайда. Так что, надеюсь, статья вам помогла!
На всякий случай, оставлю ссылку на свой публичный sample-проект на GitHub. Если у вас есть вопросы, задавайте их в комментариях.
Комментарии (13)
gudvinr
29.09.2022 14:04-4Go хорош как раз тем, что в нем нету этой дичи с навязыванием структуры директорий, как в Django, RoR, разных PHP и JS фреймворках.
Если часть функционала не используется - образуются ненужные директории, а иногда файлы с плейсходерами.
Чуть в сторону отойти надо - и уже приходится вкручивать костыли в эту красивую структуру.Не надо заниматься усложнением и пытаться формализовать то, что это не требовало
Ionenice
29.09.2022 16:24+2Вы сравниваете язык и фреймворки других языков?
sandryunin
29.09.2022 16:51-3у фреймворков в ГО, по типу gorm и тому подобных тоже нет таких требований
qRoC
29.09.2022 21:23+2На Go есть фреймворки? Поделитесь?
PS: Почитайте что такое фреймворк.
sandryunin
30.09.2022 13:34-1ну если отталкиваться от определения что есть фреймворк то конечно нет, и не будет наверное ибо противоречит идеологии, но то для чего обычно используют фреймворки есть отдельными либами, gorm, mux и прочие
sandryunin
30.09.2022 13:54ну и специально для вас
https://github.com/cosmos/cosmos-sdk
https://github.com/oklahomer/go-sarah
и еще куча фреймворков которые так же можно нагуглить
gudvinr
30.09.2022 18:43Ну так и автор зачем-то в язык притягивает то, чем изготовители фреймворков занимаются. В этом и суть, что Go — это не фреймворк, и натягивать какую-то формальную структуру смысла нет, даже если вы пишете бэкенд, а не консольное приложение, например.
И что характерно, даже Go фреймворки не требуют какую-то жёсткую привязку к структуре директорий.
borshak
30.09.2022 04:17+1С версии Go 1.4 определен механизм, который не позволяет импортировать пакеты вне данного проекта, если они находятся внутри /internal.
Вот прямо внутрь компилятора встроена проверка, что в пути к импортируемому пакету не встречается папка internal?Avksentii Автор
30.09.2022 11:15+1Привет! это реализовано на уровне языка. Есть специальный go-инструмент, который распознает
internal
каталог и предотвращает импорт одного пакета другим, если оба не имеют общего предка. Более подробная инфа здесь - https://docs.google.com/document/d/1e8kOo3r51b2BWtTs_1uADIA5djfXhPT36s6eHVRIvaU/edit
sandryunin
Скажите, вот зачем вам в проект тащить конфиги, деплойменты и вообще все то что относиться к DevOps, во первых хранить конфиги рядом с проектом так себе идея, хранить примерный не рабочий конфиг конечно можно, да и обычный никто не запрещает, но есть критерии безопасности, внеся в конфиг что то важное можно это и закомитить ненароком.
Далее деплойменты, я понимаю конечно что это удобно например приходит новый разраб сделал чекаут и может уже куда то задеплоить, например в неймспейс кубера который ему выдали. Или локально. Я ничего не имею против композ файла, но хельм чарты там зачем?
Вообще микросервисов может быть много все они деплоиться могут +- одинаково или разно, да и вообще это девопс задача. И пусть они ими и занимаются, и дают отдельный доступ к отдельному репо с конфигом и деплойментом.
По моему мнению в репозитории с кодом должен лежать только код, и документация к нему. А все что касается его доставки должно жить отдельно. Лично мое мнение мешать мух и котлет можно, если у вас демо проект для обучения или демонстрации возможностей или продукта. Тогда я не спорю, это удобно сам так делаю, прикладываю конфиги и композ файлы, так как изначально знаю что ссылку на проект получит мой коллега имеющий меньший опыт чем я, и для простоты работы для него сделано все вместе, так как он только обучается. Но когда у нас коммерческий проект имхо это лишнее, пусть разрабы пишут код а девопсы его катят...
143672
Появится новая переменная, зависимая от окружения, часть конфигурации. Получается на каждое такое изменение нужно будет дергать девопса или лезть в другую репу?
sandryunin
Для этого полно решений, например Ansible Tower и передача переменных из варсов
creker
Почему нет. Если придерживаться современного gitops подхода, то все так и будет. Один репозиторий с кодом и докерфайлами под каждый микросервис или монорепо под все. Другой с хельм чартами и манифестами для деплоя.