При запуске нового проекта самые большие трудности у меня всегда вызывала его настройка. Всегда стараешься сделать её «идеальной»:
- используешь лучшую структуру каталогов, чтобы всё было легко найти и импортирование происходило без проблем;
- настраиваешь все команды так, чтобы нужные действия выполнялись в один клик или с вводом одной команды;
- находишь лучший инструмент контроля качества кода, средство форматирования, среду тестирования для используемого в проекте языка и библиотеки…
Этот список можно продолжать и продолжать, и всё равно до идеальной настройки будет ещё далеко… Но, по моему скромному мнению, эта настройка для Golang просто лучшая!
Она так хорошо себя проявляет отчасти и потому, что основана на существующих проектах, которые вы можете найти здесь и тут.
Краткое изложение доступно в моём репозитории — https://github.com/MartinHeinz/go-project-blueprint
Структура каталогов
Первым делом обратимся к структуре каталогов нашего проекта. Здесь у нас несколько файлов верхнего уровня и четыре каталога:
pkg
— это пакет Go, который содержит только строку версии global. Меняется на версию из хэша текущей фиксации при проведении сборки;config
— конфигурационный каталог, который содержит файлы со всеми необходимыми переменными среды. Вы можете использовать любой тип файла, но я бы рекомендовал файлы YAML: их проще читать;build
— в этой директории у нас все скрипты оболочки, необходимые для сборки и тестирования приложения, а также создания отчётов для инструментов анализа кода;cmd
— фактический исходный код. По правилам именования исходный каталог называетсяcmd
. Внутри есть ещё один каталог с именем проекта (в нашем случаеblueprint
). В свою очередь, внутри этого каталога находитсяmain.go
, запускающий всё приложение. Также здесь можно найти все остальные исходные файлы, разделённые на модули (подробнее об этом далее).
Оказывается, многие предпочитают помещать исходный код в каталоги internal
и pkg
. Я думаю, что это лишнее: достаточно использовать для этого cmd
, где для всего есть своё место.
Помимо каталогов, есть ещё большое количество файлов, о которых мы поговорим в статье.
Модули Go для идеального управления зависимостями
В проектах Go используются самые разные стратегии управления зависимостями. Однако с версии 1.11 Go обзавёлся официальным решением. Все наши зависимости приводятся в файле go.mod
, в корневом каталоге. Вот как он может выглядеть:
module github.com/MartinHeinz/go-project-blueprint go 1.12 require ( github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.4.0 )
Вы можете спросить: «А как в этот файл включить зависимости?». Очень просто, всего одной командой:
go mod vendor
Эта команда переустанавливает vendor
каталог основного модуля для включения всех пакетов, необходимых для сборки и тестирования каждого пакета модуля исходя из состояния файлов go.mod
и исходного кода Go.
Фактический исходный код и конфигурация
И вот наконец мы добрались до исходного кода. Как уже говорилось, исходный код разделён на модули. Модуль представляет собой каталог внутри исходного корневого каталога. В каждом модуле находятся исходные файлы вместе с соответствующими файлами тестов. Например:
./cmd/ └── blueprint ├── apis <- Module │ ├── apis_test.go │ ├── user.go │ └── user_test.go ├── daos <- Module │ ├── user.go │ └── user_test.go ├── services <- Module │ ├── user.go │ └── user_test.go ├── config <- Module │ └── config.go └── main.go
Такая структура способствует лучшей читаемости и лёгкости сопровождения кода: он идеально разделён на части, которые проще просматривать. Что касается конфигурации, в этой настройке используем библиотеку конфигураций Go Viper, которая может иметь дело с разными форматами, параметрами командной строки, переменными среды и т.д.
Посмотрим, как мы используем этот Viper здесь. Вот пакет config
:
var Config appConfig type appConfig struct { // Пример переменной, загружаемой в функции LoadConfig ConfigVar string } // LoadConfig загружает конфигурацию из файлов func LoadConfig(configPaths ...string) error { v := viper.New() v.SetConfigName("example") // <- имя конфигурационного файла v.SetConfigType("yaml") v.SetEnvPrefix("blueprint") v.AutomaticEnv() for _, path := range configPaths { v.AddConfigPath(path) // <- // путь для поиска конфигурационного файла в } if err := v.ReadInConfig(); err != nil { return fmt.Errorf("failed to read the configuration file: %s", err) } return v.Unmarshal(&Config) }
Он состоит из единственного файла. Объявляет один struct
, который содержит все переменные конфигурации и имеет одну функцию LoadConfig
, которая загружает конфигурацию. Требуется путь до конфигурационных файлов, в нашем случае используем путь до каталога config
, который находится в корневом каталоге проекта и содержит наши YAML файлы. И как их будем использовать? Запустим первым делом в main.go
:
if err := config.LoadConfig("./config"); err != nil { panic(fmt.Errorf("invalid application configuration: %s", err)) }
Простое и быстрое тестирование
Что важнее всего после кода? Тесты. Чтобы писать много хороших тестов, нужна настройка, с которой это будет делать легко. Для этого мы используем цель Makefile
под названием test
, которая собирает и выполняет все тесты в подкаталогах cmd
(все файлы с расширением _test.go
). Эти тесты кэшируются, так что их запуск происходит только при наличии изменений в соответствующей части кода. Это очень важно: если тесты будут слишком медленными, рано или поздно вы перестанете их запускать и сопровождать. Помимо модульного тестирования, make test
помогает следить за общим качеством кода, запуская с каждым тестовым прогоном gofmt
и go vet
. gofmt
способствует правильному форматированию кода, а go vet
помогает с помощью эвристических алгоритмов выявлять в коде любые подозрительные конструкции. Вот пример того, что может получиться в результате выполнения:
[email protected]:~$ make test Running tests: ok github.com/MartinHeinz/go-project-blueprint/cmd/blueprint (cached) ? github.com/MartinHeinz/go-project-blueprint/cmd/blueprint/config [no test files] ? github.com/MartinHeinz/go-project-blueprint/pkg [no test files] Checking gofmt: FAIL - the following files need to be gofmt'ed: cmd/blueprint/main.go Checking go vet: FAIL # github.com/MartinHeinz/go-project-blueprint/cmd/blueprint cmd/blueprint/main.go:19:7: assignment copies lock value to l: sync.Mutex Makefile:157: recipe for target 'test' failed make: *** [test] Error 1
Запуск всегда в Docker
Многие говорят, что у них запуск невозможен в облаке, а только на компьютере. Здесь есть простое решение: всегда запускаться в контейнере docker
. Делаете ли вы сборку, запускаете ли или тестируете — делайте всё это в контейнере. Кстати, что касается тестирования, make test
выполняется тоже только в docker
.
Посмотрим, как это происходит. Начнём с файлов Dockerfile
из корневого каталога проекта: один из них для тестирования (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
Долгое время Make-файлы казались мне страшными (до этого я сталкивался с ними лишь при работе с кодом C
), но на самом деле ничего страшного здесь нет, и их много где можно использовать, в том числе для этого проекта! Посмотрим, какие цели у нас здесь есть в Makefile
:
make test
— первая в рабочем потоке — собранное приложение — создаёт исполняемый двоичный код в каталогеbin
:
@echo "making $(OUTBIN)" @docker run # <- Это `докерный запуск` -i # скрытая команда --rm # <- Удаляем контейнер по завершении -u $$(id -u):$$(id -g) # <- Используем текущего пользователя -v $$(pwd):/src # <- Подключаем исходную папку -w /src # <- Устанавливаем рабочий каталог -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin # <- Подключаем каталоги -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) # с выводом двоичных данных -v $$(pwd)/.go/cache:/.cache --env HTTP_PROXY=$(HTTP_PROXY) --env HTTPS_PROXY=$(HTTPS_PROXY) $(BUILD_IMAGE) /bin/sh -c " # <- Запускаем скрипт сборки ARCH=$(ARCH) # (Проверяет на наличие OS=$(OS) # аргументов, устанавливает VERSION=$(VERSION) # переменные среды и запускает ./build/build.sh # `go install`) " @if ! cmp -s .go/$(OUTBIN) $(OUTBIN); then # <- Если двоичные данные изменились mv .go/$(OUTBIN) $(OUTBIN); # перемещаем их из `.go` в `bin` date >[email protected]; fi
make test
— тестовая — она снова использует почти тот жеdocker run
, только здесь ещё есть скриптtest.sh
(покажем только то, что нас интересует):
TARGETS=$(for d in "[email protected]"; do echo ./$d/...; done) go test -installsuffix "static" ${TARGETS} 2>&1 ERRS=$(find "[email protected]" -type f -name *.go | xargs gofmt -l 2>&1 || true) ERRS=$(go vet ${TARGETS} 2>&1 || true)
Эти строчки — важная часть файла. Первая из них собирает тестовые цели, где в качестве параметра указан путь. Вторая строчка запускает тесты и выводит документацию по тестированию ПО. Оставшиеся две строчки запускают gofmt
и go vet
. Они собирают и выводят ошибки (если таковые имеются).
make container
— и, наконец, важнейшая часть — создание развёртываемого контейнера:
.container-$(DOTFILE_IMAGE): bin/$(OS)_$(ARCH)/$(BIN) in.Dockerfile @sed -e 's|{ARG_BIN}|$(BIN)|g' -e 's|{ARG_ARCH}|$(ARCH)|g' -e 's|{ARG_OS}|$(OS)|g' -e 's|{ARG_FROM}|$(BASEIMAGE)|g' in.Dockerfile > .dockerfile-$(OS)_$(ARCH) @docker build -t $(IMAGE):$(TAG) -t $(IMAGE):latest -f .dockerfile-$(OS)_$(ARCH) . @docker images -q $(IMAGE):$(TAG) > [email protected]
Код для этой цели довольно прост: сначала он подставляет переменные в in.Dockerfile
, а затем запускает docker build
для получения образа с «изменёнными» и «последними» тегами. И дальше передаёт имя контейнера на стандартный вывод.
- Теперь, когда у нас есть образ, нужно где-то его хранить. С этим нам помогает
make push
, который помещает образ в хранилище образов Docker registry. make ci
— ещё один способ использоватьMakefile
— задействовать его в процессах непрерывной интеграции и развёртывания приложений (об этом речь пойдёт далее). Эта цель очень похожа наmake test
: тоже запускает все тесты и плюс к этому генерирует отчёты о покрытии, которые потом используются как вводная информация при проведении анализа кода.make clean
— и, наконец, если нам нужно провести очистку проекта, запускаемmake clean
, который удалит все файлы, сгенерированные предыдущими целями.
Остальные цели можно объединить в две группы: первые не так важны для нормального рабочего процесса, а вторые являются лишь частью других целей, поэтому о них можно не упоминать.
Интеграция и развёртывание ПО для идеальной разработки
Завершаем статью важной частью — процессом непрерывной интеграции и развёртывания приложений. Не буду подробно расписывать, что в нем такого — вы и сами прекрасно сможете разобраться (практически в каждой строке есть комментарий, так что всё должно быть понятно):
# Matrix build запускает 4 параллельные сборки matrix: include: - language: go # Сборка и тестирование sudo: required services: - docker script: - export GO111MODULE=on - go mod vendor # Загружаем зависимости - make build # Собираем приложение - test -f bin/linux_amd64/blueprint # Тест на наличие двоичных данных, полученных на предыдущем этапе - make all-container # Создаём все докерные контейнеры - docker images | grep "^docker.pkg.github.com/martinheinz/go-project-blueprint/blueprint.*__linux_amd64" # Проверяем наличие созданных образов - make test # Запускает тесты внутри тестового образа - language: go # SonarCloud addons: sonarcloud: organization: martinheinz-github token: secure: "tYsUxue9kLZWb+Y8kwU28j2sa0pq20z2ZvZrbKCN7Sw0WGtODQLaK9tZ94u1Sy02qL5QcabukbENmbvfouzXf4EfaKjDmYH9+Ja22X26MfTLVpaCDTQEGmNyREOFCHpjNXPgDMv1C70By5U+aPWSYF/lehB5rFijwCf7rmTFRNUDeotCTCuWb2dIkrX2i6raVu34SvqqGxKQmmH+NPLe7uKO/wXqH+cWQH1P9oJYeVksNGruw4M0MznUeQHeJQYpTLooxhEEzYiBbkerWGDMwBdZdPQwVrO2b8FEDRw/GWTFoL+FkdVMl4n4lrbO/cQLbPMTGcfupNCuVHh1n8cGp8spMkrfQGtKqvDRuz2tBs0n1PWXCRS6pgZQw/ClLPgi/vVryVRwOabIHSQQLRVhcdp8pkYdyX3aH1EdlIHiJLT6sacS0vJPqZMF/HNsPEoHe4YdiYvx/tcYMU63KQVZzgF4HfQMWy69s1d0RZUqd+wrtHU1DHwnkq1TSe+8nMlbvbmMsm6FVqGistrnVjx4C9TjDWQcjprYU40zCvc1uvoSPimVcaD8ITalCDHlEfoV7wZuisV8+gJzOh9pDZ/joohW7/P3zklGgI2sH7qt62GE4o5UyRArzJC7eIj7Oxx6GdbeEqw09M4rCfR1g5tHWIqVHz5CajvkXkPqrRGu2oI=" before_script: - ./reports.sh # Создаёт каталоги и файлы для отчётов - export GO111MODULE=on - go mod vendor # Загружаем зависимости - make ci # Запускаем тесты и генерируем отчёты (см. этап `ci` в Makefile) script: - sonar-scanner # Запускаем анализ с помощью плагина SonarCloud scanner plugin - language: go # CodeClimate before_script: - ./reports.sh # Создаём каталоги и файлы для отчётов - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter # Загружаем генератор тестовых отчётов CodeClimate - chmod +x ./cc-test-reporter # Делаем его исполняемым - ./cc-test-reporter before-build # Уведомляем CodeClimate о готовом отчёте script: - export GO111MODULE=on - go mod vendor # Загружаем зависимости - make ci # Запускаем тесты и генерируем отчёты (см. этап `ci` в Makefile) after_script: - ./cc-test-reporter after-build -t gocov --exit-code $TRAVIS_TEST_RESULT # Отправляем отчёт в CodeClimate или уведомляем о неудавшейся сборке кодом завершения - language: go # Помещаем контейнер в хранилище services: - docker if: branch = master script: - export GO111MODULE=on - go mod vendor # Загружаем зависимости - echo "$DOCKER_PASSWORD" | docker login docker.pkg.github.com -u "$DOCKER_USERNAME" --password-stdin # Подключаемся ко хранилищу GitHub Registry, используя переменные среды Travis - make container # Создаём изменённые и последние образы - make push # Помещаем образ в хранилище notifications: email: false
Но кое-что можно прояснить.
В этой сборке Travis использована сборка Matrix Build с 4 параллельными заданиями для ускорения всего процесса:
- Сборка и тестирование: здесь мы проверяем, что приложение работает как надо;
- SonarCloud: здесь мы генерируем отчёты о покрытии и отправляем их на сервер SonarCloud;
- CodeClimate: здесь — как и в предыдущем задании — мы генерируем отчёты о покрытии и отправляем их, только на этот раз в CodeClimate с помощью их генератора тестовых отчётов;
- Push to Registry: и, наконец, помещаем наш контейнер в хранилище GitHub Registry.
Заключение
Надеюсь, эта статья поможет вам в ваших будущих разработках кода на Go. Все подробности изложены в репозитории.
В следующей части узнаем, как на базе этого макета проекта, который мы сегодня выстроили, с лёгкостью создавать интерфейсы RESTful API, тестировать с базой данных в памяти, а также настраивать крутую документацию (а пока можно подсмотреть в ветке rest-api
репозитория). 🙂
Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming