Как структурировать проект на Golang: гайд от backend-разработчика +15


Всем привет, меня зовут Авксентий, я backend-разработчик в inDriver. Думаю, каждый начинающий разработчик сталкивался с проблемой, как правильно выстроить архитектуру и структуру проекта. Ведь организация кода проекта — постоянно развивающаяся проблема, а следование стандартной структуре сохраняет чистоту кода и повышает производительность команды. 

Когда я начинал писать на Go, то потратил много времени на поиски стандартов структурирования проекта. В итоге так и не нашел официального и точного стандарта — либо информация была неполной, либо это было не то, что нужно. Я решил написать свой гайд на основе опыта. Он для начинающих разработчиков и посвящен тому, как структурировать проект на Golang.

Почему я решил написать эту статью

Правильно выстроенные архитектура и структура проекта в начале способствует безболезненной разработке, масштабированию и легкому внедрению новых разработчиков в проект. Конечно, плоскую структуру тоже можно использовать для маленьких проектов с одним main-файлом, но это непрактично для больших.

Я видел немало проектов, и из каждого взял что-то для себя. Рекомендую ознакомиться с этим проектом — многие ориентируется на него при создании структуры. В моем примере будет более компактная структура — мы детально разберем директорию /internal.

Конечно, метод не претендует на лучший или единственный способ ведения дел. Но, думаю, он хорошо подойдет для начала. Посмотрим на структуру корня проекта:

Корневая структура приложения
Корневая структура приложения

Директории

/cmd

Точка входа для нашего приложения. Имя директории для каждого приложения должно совпадать с именем исполняемого файла, который вы хотите собрать. Не стоит располагать в этой директории много кода. Самой распространенной практикой является использование маленькой main-функции, которая импортирует и вызывает весь необходимый код из директорий /internal и /pkg.

Структура директории /cmd
Структура директории /cmd

/internal

Сердце нашего приложения — всю внутреннюю логику приложения храним здесь. /internal не импортируем в других приложениях и библиотеках. Код, который написан тут, предназначен исключительно для внутреннего использования в рамках кодовой базы. С версии Go 1.4 определен механизм, который не позволяет импортировать пакеты вне данного проекта, если они находятся внутри /internal. 

В /internal мы храним бизнес-логику проекта и работу с базами данными. В общем, всю логику, связанную с этим приложением. Выстроить структуру внутри /internal можно по-разному, в зависимости от архитектуры. Но я не буду сильно углубляться в нее, а поверхностно покажу, как это выглядит. Приведу пример трехуровневой архитектуры, когда приложение делится на 3 слоя:

  1. Транспортный.

  2. Бизнес.

  3. Базы данных.

Логика должна быть выстроена так, чтобы слои иерархически обращались друг к другу сверху вниз и наоборот. Не допускается, чтобы один слой перескакивал через промежуточный (например, транспортный напрямую в базу данных) и нижний не общался с верхним (например, база данных ходит в транспортный слой).

Модель трехуровневой архитектуры
Модель трехуровневой архитектуры

Транспортный слой:

Сетевой уровень приложения, на котором конечный пользователь взаимодействует с приложением. После обработки запроса вся собранная информация идет на слой ниже.

Бизнес-слой:

Как следует из названия, содержит бизнес-логику, которая поддерживает основные функции приложения. Если в логике затрагиваются базы данных, мы идем на слой ниже.

Слой базы данных:

Отвечает за взаимодействие с постоянными хранилищами, такими как базы данных, и прочую обработку информации, которая не связана с бизнесом. Например, чтение и запись в базе данных.

Директории /internal:

  • /app
    Точка, где все наши зависимости и логика собираются и запускают приложение. Run-метод, который вызывается из /cmd.

  • /config
    Инициализация общих конфигураций приложения, которые мы прописывали в корне проекта.

  • /database (слой базы данных)
    Файлы содержат методы для взаимодействия с базами данных.

  • /models (слой базы данных)
    Структуры таблиц баз данных.

  • /services (бизнес-слой)
    Вся бизнес-логика приложения.

  • /transport (транспортный слой)
    Здесь храним http-настройки сервера, хендлеры, порты и так далее.

Структура директории /internal
Структура директории /internal

/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):

  1. sandryunin
    /#24776012 / +5

    Скажите, вот зачем вам в проект тащить конфиги, деплойменты и вообще все то что относиться к DevOps, во первых хранить конфиги рядом с проектом так себе идея, хранить примерный не рабочий конфиг конечно можно, да и обычный никто не запрещает, но есть критерии безопасности, внеся в конфиг что то важное можно это и закомитить ненароком.

    Далее деплойменты, я понимаю конечно что это удобно например приходит новый разраб сделал чекаут и может уже куда то задеплоить, например в неймспейс кубера который ему выдали. Или локально. Я ничего не имею против композ файла, но хельм чарты там зачем?

    Вообще микросервисов может быть много все они деплоиться могут +- одинаково или разно, да и вообще это девопс задача. И пусть они ими и занимаются, и дают отдельный доступ к отдельному репо с конфигом и деплойментом.

    По моему мнению в репозитории с кодом должен лежать только код, и документация к нему. А все что касается его доставки должно жить отдельно. Лично мое мнение мешать мух и котлет можно, если у вас демо проект для обучения или демонстрации возможностей или продукта. Тогда я не спорю, это удобно сам так делаю, прикладываю конфиги и композ файлы, так как изначально знаю что ссылку на проект получит мой коллега имеющий меньший опыт чем я, и для простоты работы для него сделано все вместе, так как он только обучается. Но когда у нас коммерческий проект имхо это лишнее, пусть разрабы пишут код а девопсы его катят...

    • 143672
      /#24776038

      Появится новая переменная, зависимая от окружения, часть конфигурации. Получается на каждое такое изменение нужно будет дергать девопса или лезть в другую репу?

      • sandryunin
        /#24776658

        Для этого полно решений, например Ansible Tower и передача переменных из варсов

      • creker
        /#24781108

        Почему нет. Если придерживаться современного gitops подхода, то все так и будет. Один репозиторий с кодом и докерфайлами под каждый микросервис или монорепо под все. Другой с хельм чартами и манифестами для деплоя.

  2. gudvinr
    /#24776468 / -4

    Go хорош как раз тем, что в нем нету этой дичи с навязыванием структуры директорий, как в Django, RoR, разных PHP и JS фреймворках.

    Если часть функционала не используется - образуются ненужные директории, а иногда файлы с плейсходерами.
    Чуть в сторону отойти надо - и уже приходится вкручивать костыли в эту красивую структуру.

    Не надо заниматься усложнением и пытаться формализовать то, что это не требовало

    • Ionenice
      /#24776876 / +2

      Вы сравниваете язык и фреймворки других языков?

      • sandryunin
        /#24776946 / -3

        у фреймворков в ГО, по типу gorm и тому подобных тоже нет таких требований

        • qRoC
          /#24777470 / +2

          На Go есть фреймворки? Поделитесь?

          PS: Почитайте что такое фреймворк.

          • sandryunin
            /#24779204 / -1

            ну если отталкиваться от определения что есть фреймворк то конечно нет, и не будет наверное ибо противоречит идеологии, но то для чего обычно используют фреймворки есть отдельными либами, gorm, mux и прочие

      • gudvinr
        /#24780184

        Ну так и автор зачем-то в язык притягивает то, чем изготовители фреймворков занимаются. В этом и суть, что Go — это не фреймворк, и натягивать какую-то формальную структуру смысла нет, даже если вы пишете бэкенд, а не консольное приложение, например.


        И что характерно, даже Go фреймворки не требуют какую-то жёсткую привязку к структуре директорий.

  3. borshak
    /#24778012 / +1

    С версии Go 1.4 определен механизм, который не позволяет импортировать пакеты вне данного проекта, если они находятся внутри /internal.

    Вот прямо внутрь компилятора встроена проверка, что в пути к импортируемому пакету не встречается папка internal?

    • Avksentii
      /#24778574 / +1

      Привет! это реализовано на уровне языка. Есть специальный go-инструмент, который распознает internal каталог и предотвращает импорт одного пакета другим, если оба не имеют общего предка. Более подробная инфа здесь - https://docs.google.com/document/d/1e8kOo3r51b2BWtTs_1uADIA5djfXhPT36s6eHVRIvaU/edit