В этом руководстве мы рассмотрим, как разработчик Go может использовать Makefile при разработке собственных приложений.
Что такое Makefile-ы?
Makefile — невероятно полезный инструмент автоматизации, который можно использовать для запуска и сборки приложений не только на Go, но и на большинстве других языков программирования.
Его часто можно увидеть в корневом каталоге множества Go приложений на Github и Gitlab. Он широко используются в качестве инструмента для автоматизации задач, которые часто сопровождают разработчиков.
Если вы используете Go для создания веб-сервисов, то Makefile поможет решить следующие задачи:
- Автоматизация вызова простых команд, таких как: compile, start, stop, watch и т. д.
- Управление специфичными для проекта переменными окружения. Он должен подключать файл .env.
- Режим разработки, который автоматически компилируется при изменении.
- Режим разработки, который показывает ошибки компиляции.
- Определение GOPATH для конкретного проекта, чтобы мы могли хранить зависимости в папке vendor.
- Упрощенный мониторинг файлов, например, make watch run = «go test. / ...»
Вот типичная структура каталогов для проекта:
.env
Makefile
main.go
bin/
src/
vendor/
Если мы вызовем команду make в этом каталоге, то получим следующий вывод:
$ make
Choose a command run in my-web-server:
install Install missing dependencies. Runs `go get` internally.
start Start in development mode. Auto-starts when code changes.
stop Stop development mode.
compile Compile the binary.
watch Run given command when code changes. e.g; make watch run="go test ./..."
exec Run given command, wrapped with custom GOPATH. e.g; make exec run="go test ./..."
clean Clean build files. Runs `go clean` internally.
Переменные окружения
Первое, чего мы хотим от Makefile — подключать переменные окружения, которые мы определили для проекта. Поэтому первая строчка будет выглядеть так:
include .env
Далее мы определяем имя проекта, папки/файлы Go, пути к pid…
PROJECTNAME=$(shell basename "$(PWD)")
# Go переменные.
GOBASE=$(shell pwd)
GOPATH=$(GOBASE)/vendor:$(GOBASE):/home/azer/code/golang #Вы можете удалить или изменить путь после двоеточия.
GOBIN=$(GOBASE)/bin
GOFILES=$(wildcard *.go)
# Перенаправление вывода ошибок в файл, чтобы мы показывать его в режиме разработки.
STDERR=/tmp/.$(PROJECTNAME)-stderr.txt
# PID-файл будет хранить идентификатор процесса, когда он работает в режиме разработки
PID=/tmp/.$(PROJECTNAME)-api-server.pid
# Make пишет работу в консоль Linux. Сделаем его silent.
MAKEFLAGS += --silent
В оставшейся части Makefile мы будем часто использовать переменную GOPATH. Все наши команды должны быть связаны с GOPATH конкретного проекта, иначе они не будут работать. Это обеспечивает чистую изоляцию наших проектов, но при этом усложняет работу. Чтобы упростить задачу, мы можем добавить команду exec, которая выполнит любую команду с нашим GOPATH.
# exec: Запускает команду с кастомным GOPATH. Пример: make exec run = " go test ./...”
exec:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) $(run)
Однако стоит помнить, что использовать exec нужно только в том случае, если требуется сделать то, что нельзя прописать в makefile.
Режим разработки
Режим разработки должен:
- Очищать кеш сборки
- Компилировать код
- Запускать сервис в бэкграунде
- Повторять это шаги, когда код изменяется.
Звучит просто. Однако, сложность заключается в том, что мы одновременно запускаем и сервис, и файл-watcher. Перед запуском нового процесса, мы должны обеспечить корректную остановку, а также не нарушить обычное поведение командной строки при нажатии Control-C или Control-D.
start:
bash -c "trap 'make stop' EXIT; $(MAKE) compile start-server watch run='make compile start-server'"
stop: stop-server
Описанный выше код решает следующие задачи:
- Компилирует и запускает сервис в фоновом режиме.
- Основной процесс работает не в фоновом режиме, поэтому мы можем его прервать, используя Control-C.
- Останавливает фоновые процессы, когда основной процесс прерывается. trap нужна как раз для этого.
- Рекомпилирует и перезапускает сервер при изменении кода.
В следующих разделах я объясню эти команды подробнее.
Компиляция
Команда compile не просто вызывает go compile в фоновом режиме — она очищает вывод ошибок и печатает упрощенную версию.
Вот как выглядит вывод командной строки, когда мы внесли «ломающие» правки:
compile:
@-touch $(STDERR)
@-rm $(STDERR)
@-$(MAKE) -s go-compile 2> $(STDERR)
@cat $(STDERR) | sed -e '1s/.*/nError:n/' | sed 's/make[.*/ /' | sed "/^/s/^/ /" 1>&2
Запуск/остановка сервера
start-server запускает бинарник, скомпилированный в фоновом режиме, сохраняя свой PID во временный файл. stop-server читает PID и убивает процесс при необходимости.
start-server:
@echo " > $(PROJECTNAME) is available at $(ADDR)"
@-$(GOBIN)/$(PROJECTNAME) 2>&1 & echo $$! > $(PID)
@cat $(PID) | sed "/^/s/^/ > PID: /"
stop-server:
@-touch $(PID)
@-kill `cat $(PID)` 2> /dev/null || true
@-rm $(PID)
restart-server: stop-server start-server
Мониторинг изменений
Нам нужен файл-watcher для отслеживания изменений. Я перепробовал многие, но не смог найти подходящего, поэтому написал свой собственный инструмент для мониторинга файлов — yolo. Установите его с помощью команды:
$ go get github.com/azer/yolo
После установки мы можем наблюдать за изменениями в каталоге проекта, исключая папки vendor и bin.
## watch: Запустите данную команду при изменении кода, например make watch run="echo 'hey'"
watch:
@yolo -i . -e vendor -e bin -c $(run)
Теперь у нас есть команда watch, которая рекурсивно отслеживает изменения в каталоге проекта, за исключением каталога vendor. Мы можем просто передать любую команду в run.
Например, start вызывает make-start-server при изменении кода:
make watch run="make compile start-server"
Мы можем использовать его для запуска тестов или проверки race conditions автоматически. Переменные окружения будут установлены при исполнении, поэтому вам не нужно беспокоиться о GOPATH:
make watch run="go test ./..."
Приятной особенностью Yolo является его веб-интерфейс. Если его включить, вы сможете сразу увидеть вывод вашей команды в веб-интерфейсе. Все, что вам нужно, это передать параметр -a:
yolo -i . -e vendor -e bin -c "go run foobar.go" -a localhost:9001
Откройте localhost: 9001 в браузере и сразу же увидите результат работы:
Установка зависимостей
Когда мы вносим изменения в код, мы бы хотели, чтобы отсутствующие зависимости были загружены до компиляции. Команда install сделает эту работу за нас:
install: go-get
Мы будем автоматизировать вызов install при изменении файла перед компиляцией, поэтому зависимости будут установлены автоматически. Если вы хотите установить зависимость вручную, можете запустить:
make install get="github.com/foo/bar"
Внутри эта команда будет преобразована в:
$ GOPATH=~/my-web-server GOBIN=~/my-web-server/bin go get github.com/foo/bar
Как это работает? Смотрите следующий раздел, где мы добавляем обычные команды Go для реализации команд более высокого уровня.
Команды Go
Поскольку мы хотим установить GOPATH в каталог проекта, чтобы упростить управление зависимостями, которое до сих пор официально не решено в экосистеме Go, нам нужно обернуть все команды Go в Makefile.
go-compile: go-clean go-get go-build
go-build:
@echo " > Building binary..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES)
go-generate:
@echo " > Generating dependency files..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate)
go-get:
@echo " > Checking if there is any missing dependencies..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get)
go-install:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES)
go-clean:
@echo " > Cleaning build cache"
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean
Help
Наконец, нам нужна команда help, чтобы увидеть список доступных команд. Мы можем автоматически генерировать красиво отформатированный вывод справки, используя команды sed и column:
help: Makefile
@echo " Choose a command run in "$(PROJECTNAME)":"
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
Следующая команда сканирует Makefile на строки, начинающиеся с ##, и выводит их. Таким образом, вы можете просто комментировать определенные команды, и комментарии будут выводиться командой help.
Если мы добавим несколько комментариев:
## install: Install missing dependencies. Runs `go get` internally.
install: go-get
## start: Start in development mode. Auto-starts when code changes.
start:
## stop: Stop development mode.
stop: stop-server
Мы получим:
$ make help
Choose a command run in my-web-server:
install Install missing dependencies. Runs `go get` internally.
start Start in development mode. Auto-starts when code changes.
stop Stop development mode.
Окончательный вариант
include .env
PROJECTNAME=$(shell basename "$(PWD)")
# Go related variables.
GOBASE=$(shell pwd)
GOPATH="$(GOBASE)/vendor:$(GOBASE)"
GOBIN=$(GOBASE)/bin
GOFILES=$(wildcard *.go)
# Redirect error output to a file, so we can show it in development mode.
STDERR=/tmp/.$(PROJECTNAME)-stderr.txt
# PID file will keep the process id of the server
PID=/tmp/.$(PROJECTNAME).pid
# Make is verbose in Linux. Make it silent.
MAKEFLAGS += --silent
## install: Install missing dependencies. Runs `go get` internally. e.g; make install get=github.com/foo/bar
install: go-get
## start: Start in development mode. Auto-starts when code changes.
start:
bash -c "trap 'make stop' EXIT; $(MAKE) compile start-server watch run='make compile start-server'"
## stop: Stop development mode.
stop: stop-server
start-server: stop-server
@echo " > $(PROJECTNAME) is available at $(ADDR)"
@-$(GOBIN)/$(PROJECTNAME) 2>&1 & echo $$! > $(PID)
@cat $(PID) | sed "/^/s/^/ > PID: /"
stop-server:
@-touch $(PID)
@-kill `cat $(PID)` 2> /dev/null || true
@-rm $(PID)
## watch: Run given command when code changes. e.g; make watch run="echo 'hey'"
watch:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) yolo -i . -e vendor -e bin -c "$(run)"
restart-server: stop-server start-server
## compile: Compile the binary.
compile:
@-touch $(STDERR)
@-rm $(STDERR)
@-$(MAKE) -s go-compile 2> $(STDERR)
@cat $(STDERR) | sed -e '1s/.*/nError:n/' | sed 's/make[.*/ /' | sed "/^/s/^/ /" 1>&2
## exec: Run given command, wrapped with custom GOPATH. e.g; make exec run="go test ./..."
exec:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) $(run)
## clean: Clean build files. Runs `go clean` internally.
clean:
@(MAKEFILE) go-clean
go-compile: go-clean go-get go-build
go-build:
@echo " > Building binary..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES)
go-generate:
@echo " > Generating dependency files..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate)
go-get:
@echo " > Checking if there is any missing dependencies..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get)
go-install:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES)
go-clean:
@echo " > Cleaning build cache"
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean
.PHONY: help
all: help
help: Makefile
@echo
@echo " Choose a command run in "$(PROJECTNAME)":"
@echo
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
@echo
Автор: ldanmer