Примечание. Изначально это было размещено на martinheinz.dev

Для меня самой большой проблемой при запуске нового проекта всегда была попытка настроить проект «идеально». Я всегда стараюсь использовать лучшую структуру каталогов, чтобы все было легко найти, а импорт работал хорошо, настраиваю все команды так, чтобы я всегда находился на расстоянии одного щелчка / команды от желаемого действия, находил лучший линтер, форматировщик, среду тестирования для языка / библиотека, которую я использую…

Список можно продолжать, и он никогда не доходит до того, что я действительно доволен настройкой… за исключением этой окончательной и лучшей (ИМХО) настройки для Golang!

Примечание. Эта установка работает так хорошо отчасти потому, что она основана на существующих проектах, которые можно найти здесь и здесь.

TL; DR: Вот мой репозиторий - https://github.com/MartinHeinz/go-project-blueprint

Структура каталогов

Прежде всего, давайте рассмотрим структуру каталогов нашего проекта. Есть несколько файлов верхнего уровня, а также 4 каталога:

  • pkg - Начнем с простого - pkg - это пакет Go, который содержит только строку глобальной версии. Он заменяется фактической версией, вычисленной из хэша фиксации во время сборки.
  • config - Далее следует каталог конфигурации, в котором хранятся файлы со всеми необходимыми переменными среды. Можно использовать любой тип файла, но я рекомендую файлы YAML, поскольку они более читабельны.
  • build - Этот каталог содержит все сценарии оболочки, необходимые для создания и тестирования вашего приложения, а также для создания отчетов для инструментов анализа кода.
  • cmd - Актуальный исходный код! По соглашению исходный каталог называется cmd, внутри находится еще один с именем проекта - в данном случае blueprint. Затем внутри этого каталога находится main.go, который запускает все приложение, вместе с ним есть все другие исходные файлы, разделенные на модули (подробнее об этом позже).

Примечание. Из некоторых отзывов я выяснил, что многие люди предпочитают использовать каталоги internal и pkg для размещения всего своего исходного кода. Я лично считаю это ненужным и излишним, поэтому я помещаю все в cmd, но каждому свое.

Помимо каталогов, есть также довольно много файлов, о которых мы поговорим в следующих разделах.

Модули Go для идеального управления зависимостями

В проектах Go используется широкий спектр стратегий управления зависимостями. Однако, начиная с версии 1.11, в Go есть официальное решение для управления зависимостями. Все наши зависимости перечислены в go.mod файле, который находится в корневом каталоге. Вот как это может выглядеть:

Вы можете спросить «Каким образом файл заполняется зависимостями?». Что ж, это довольно просто, все, что вам нужно, это одна команда:

Эта команда сбрасывает каталог vendor основного модуля, чтобы включить в него все пакеты, необходимые для сборки и тестирования всех пакетов модуля, на основе состояния go.mod файлов и исходного кода Go.

Фактический исходный код и конфигурация

Теперь мы наконец подошли к исходному коду. Как было сказано выше, исходный код разделен на модули. Каждый модуль - это каталог в корне источника. В каждом модуле есть исходные файлы вместе с файлами их тестов, например:

Эта структура помогает повысить удобочитаемость и ремонтопригодность, поскольку она разделяет код на разумные фрагменты, по которым легче перемещаться. Что касается конфигурации, в этой настройке я использую Viper, которая представляет собой библиотеку конфигурации Go, которая может обрабатывать различные форматы, флаги командной строки, переменные среды и т. Д.

Так как же нам использовать его (Viper) здесь? Давайте посмотрим на пакет config:

Этот пакет состоит из одного файла. Он объявляет один struct, содержащий все переменные конфигурации, и одну функцию LoadConfig, которая, в общем, загружает config. Он принимает путь к файлам конфигурации, в нашем случае мы будем использовать путь к каталогу config, который находится в корне проекта и содержит наши файлы YAML (упомянутые выше). И как нам это использовать? Запускаем его первым делом в main.go:

Простое и быстрое тестирование

Вторая по важности вещь после кода? Тесты качества. Чтобы быть готовым написать много хороших тестов, вам нужна настройка, которая упростит вам задачу. Для этого мы будем использовать Makefile цель с именем test, которая собирает и запускает все тесты в cmd подкаталогах (все файлы с суффиксом _test.go). Эти тесты также кэшируются, поэтому они запускаются только в том случае, если в соответствующий код были внесены некоторые изменения. Это очень важно, так как если тесты будут слишком медленными, вы (скорее всего) в конечном итоге прекратите их запускать и поддерживать. Помимо модульного тестирования, make test также помогает поддерживать общее качество кода, поскольку он также запускается gofmt и go vet при каждом запуске теста. go fmt заставляет вас правильно форматировать код, а go vet обнаруживает подозрительные конструкции кода с помощью эвристики. Пример вывода:

Всегда работает в Docker

Люди часто говорят «Это работает на моей машине (а не в облаке)…», чтобы избежать этого, у нас есть простое решение - всегда запускать в контейнере докера. И когда я говорю всегда, я действительно имею в виду это - построить в контейнере, запустить в контейнере, протестировать в контейнере. На самом деле я не упоминал об этом в предыдущем разделе, но make test действительно "просто" docker run.

Итак, как это здесь работает? Начнем с Dockerfiles, которое находится в корне проекта - у нас есть два из них: один для тестирования (test.Dockerfile), а другой - для запуска приложения (in.Dockerfile):

  • test.Dockerfile - В идеале у нас был бы только один Dockerfile для запуска и тестирования приложения. Однако может потребоваться небольшая корректировка среды при запуске тестов. Вот почему у нас есть этот образ - чтобы мы могли установить дополнительные инструменты и библиотеки, если это потребуется для наших тестов. В качестве примера предположим, что у нас есть база данных, к которой мы подключаемся. Мы не хотим запускать весь сервер PostgreSQL при каждом запуске теста или зависеть от какой-либо базы данных, запущенной на хост-машине. Поэтому вместо этого мы можем использовать базу данных SQLite в памяти для наших тестовых прогонов. Но знаете что? Для двоичного файла SQLite требуется. Так что же нам делать? Мы просто устанавливаем gcc и g++, снимаем флажок CGO_ENABLED, и все готово.
  • in.Dockerfile - Если вы посмотрите на этот Dockerfile в репозитории, это просто набор аргументов и копирование конфигурации в изображение - так что там происходит? in.Dockerfile используется только из Makefile, где аргументы заполняются, когда мы запускаем make container. Теперь пришло время взглянуть на сам Makefile, который делает все docker за нас. 👇

Связываем все вместе с помощью Makefile

Долгое время Makefiles казались мне пугающими, так как я видел, как они используются только с кодом C, но они не страшны и могут использоваться для очень многих вещей, включая этот проект! Давайте теперь исследуем цели, которые есть в нашем Makefile:

  • make test - Сначала в рабочем процессе - сборка приложения - он создает двоичный исполняемый файл в каталоге bin:
  • make test - Следующий этап - тестирование - он снова использует docker run, который почти идентичен, с той лишь разницей, что test.sh скрипт (только соответствующие части):

Строки выше - важная часть файла. Первый из них собирает цели тестирования, используя путь, указанный в качестве параметра. Вторая строка запускает тесты и выводит результат на стандартный вывод. Остальные две строки запускают соответственно go fmt и go vet, собирая ошибки (если они есть) и распечатывая их.

  • make container - Теперь самая важная часть - создание контейнера, который можно развернуть:

Код для этой цели довольно прост: сначала он заменяет переменные в in.Dockerfile, а затем запускает docker build для создания изображения с тегами 'dirty' и 'latest'. Наконец, он выводит имя контейнера на стандартный вывод.

  • make push - Далее, когда у нас есть изображение, нам нужно его где-то сохранить, верно? Итак, все, что делает make push, - это помещает образ в реестр Docker.
  • make ci - Еще одно хорошее применение Makefile - использовать его внутри нашего конвейера CI / CD (следующий раздел). Эта цель очень похожа на make test - она ​​также запускает все тесты, но, кроме того, она также генерирует отчеты о покрытии, которые затем используются в качестве входных данных для инструментов анализа кода.
  • make clean - Наконец, если мы хотим очистить наш проект, мы можем запустить make clean, который удаляет все файлы, созданные предыдущими целями.

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

CI / CD для максимального опыта кодирования

И последнее, но не менее важное - CI / CD. С такой хорошей настройкой (если я сам так говорю) было бы стыдно опустить какой-то причудливый конвейер, который может сделать за нас массу вещей, не так ли? Я не буду вдаваться в подробности о том, что находится в стадии разработки, потому что вы можете проверить это сами здесь (я также включил комментарии практически для каждой строки, поэтому все объяснено), но я хочу указать на несколько вещи:

Эта сборка Travis использует Matrix Build с 4 параллельными заданиями для ускорения всего процесса.

  • Сборка и тестирование, где мы проверяем, работает ли приложение должным образом.
  • SonarCloud, где мы создаем отчеты о покрытии и отправляем их на сервер SonarCloud.
  • CodeClimate - здесь, как и в предыдущем - мы генерируем отчеты и отправляем их, на этот раз в CodeClimate, используя их тестовый репортер.
  • Отправка в реестр - наконец, мы отправляем наш контейнер в реестр GitHub (следите за сообщениями в блоге об этом!)

Заключение

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

В следующей части мы рассмотрим, как вы можете расширить этот план, чтобы легко создавать RESTful API, тестировать с помощью базы данных в памяти и настраивать документацию по swagger (вы можете подглядеть в ветке rest-api в репозитории).