В этом руководстве мы рассмотрим, как разработчик Go может использовать Makefile при разработке собственных приложений.

image

Что такое 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 в фоновом режиме — она очищает вывод ошибок и печатает упрощенную версию.

Вот как выглядит вывод командной строки, когда мы внесли «ломающие» правки:

image

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 в браузере и сразу же увидите результат работы:

image

Установка зависимостей


Когда мы вносим изменения в код, мы бы хотели, чтобы отсутствующие зависимости были загружены до компиляции. Команда 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

Комментарии (4)


  1. berez
    26.07.2019 16:57

    Не совсем понимаю, почему вы решили использовать именно make. То, что вы написали — это фактически шелл-скрипт, который не использует ничего из того, для чего make создавался: отслеживание графа зависимостей, построение промежуточных таргетов и т. п.
    Кроме того, вы используете расширения GNU make, т.е. на системах, где установлен другой вариант утилиты, вылезут ошибки.
    Еще вы не учитываете, что зависимости могут выполняться одновременно, если make запустить с ключом -j. В результате 'make -j 2 restart-server' может привести к странным результатам (а еще у вас start-server — это на самом деле restart, т.к. сервер предварительно тушится).

    Теперь о мелочах.
    Цели для make называются не как душе угодно. Есть стандарт де-факто, используемый в юниксах:
    'make' без аргументов исполняет цель по умолчанию (обычно это all). У вас — почему-то выводит помощь.
    'make all' собирает всё (обычно это все бинари и нужные им библиотеки). У вас она тоже выводит помощь.
    'make install' устанавливает собранные бинари в системе (предварительно может их собрать, если нужно). Понятно, что сия команда должна исполняться из-под рута и в общем — небезопасна. У вас же на нее повешено совершенно другое действие. Я бы ее обозвал make prepare, например.
    Единственное, что более-менее получилось интуитивно-понятным — это make clean.


  1. guryanov
    26.07.2019 18:34

    У меня вот так, используется protobuf, который надо компилить и еще нужно константы снаружи прокидывать в GO код:


    NAME=ixshot
    IMPORTBASE=gitlab.com/ixshot/ixshot
    SYSCONFIGDIR=/etc/${NAME}
    DATADIR=/usr/share/${NAME}
    LDFLAGS=-ldflags "-X ${IMPORTBASE}/pkg/consts.SysConfigDir=${SYSCONFIGDIR} -X ${IMPORTBASE}/pkg/consts.SysDataDir=${DATADIR}"
    
    all: ixshot-controller.x ixshot-processor.x
    
    ixshot-controller.x: pkg/messages/lightroomtask.pb.go
            go build ${LDFLAGS} ./cmd/ixshot-controller
    
    ixshot-processor.x: pkg/messages/lightroomtask.pb.go
            go build ${LDFLAGS} ./cmd/ixshot-processor
    
    pkg/messages/lightroomtask.pb.go: pkg/messages/lightroomtask.proto
            protoc --plugin=`which protoc-gen-go` --go_out=. pkg/messages/*.proto
    

    Всякие install и deps убрал, с ними вроде все более-менее понятно


  1. rustler2000
    27.07.2019 10:10

    Переводчику спасибо, но — src/, main.go без cmd/


  1. bat
    29.07.2019 09:58

    .env
    Makefile
    main.go
    bin/
    src/
    vendor/

    плохо согласуется с project-layout