原文出自:http://azer.bike/journal/a-good-makefile-for-go/#step-by-step
这是一份为GOweb服务器而精炼的Makefile,主要为了简化构建与管理。
我经常会调整我的Makefile来提升开发速度,而今天早上也是这其中的一次,并且我决定向大家分享结果。
总结一下就是,我使用go搭建web服务,我对makefile的期望如下:
- 高水平且简单的命令,例如; compile start stop watch 等
- 管理特定项目的环境变量,其中应包含.env文件
- 开发者模式,这样每当有改动时,会自动编译
- 在开发者模式下,编译的错误提示将不会有赘述
- 特定项目的GOPATH,因此可以保持vender中的依赖
简化文件的watch,如:make watch run=”go test ./…”
以下是我个人比较喜欢的典型的文件目录:
.env
Makefile
main.go
bin/
src/
vendor
在这个文件中输入的make命令提供以下输出:
$ make
选择一个在my-web-server运行的命令
install 安装缺失的依赖,内部运行 ‘go get’ .
start 开始开发模式.当代码发生改变时自动启动.
stop 停止开发模式.
compile 二进制编译.
watch 当代码发生变化时,运行给定的命令,如:make watch run =”go test ./…”
exec 使用自定义GOPATH包装的给定命令,如:make exec run=”go test ./…”
clean 清楚构建时的文件,内部运行go clean命令.
1. 一步一个脚印
环境变量
首先,我们希望makefile中包含所有我们为工程定义的环境变量,因此,治理我我们的第一行#1:
include .env
在制定项目的环境变量之上,我们将定义其他一些东西,如项目名称,go文件/文件夹,pid文件路径…
PROJECTNAME=\$(shell basename "\$(PWD)")
# 与go相关的变量
GOBASE = \$(shell pwd)
GOPATH = \$(GOBASE)/vendor:\$(GOBASE)
GOBIN = \$(GOBASE)/bin
GOFILES=\$(wildcard *.go)
# 重定向错误输出文件,我们能在开发模式下查看
STDERR = /tmp/.\$(PROJECTNAME)-stderr.txt
# 当在开发模式下运行时,PID文件将存储服务进程id
PID = /tmp/.\$(PROJECTNAME)-api-server.pid
# make在linux中十分冗长,将其设为silent
MAKEFLAGS += --silent
在余下的makefile中,我们将着重使用特别GOPATH变量。所有我们使用的命令应该被包装在项目指定的GOPATH中,否则其将不能运行。这是我们的go项目之前有了明确的隔离,也带来了一些复杂性。为了使事情变得更加简单,我们可以添加exec命令,使用上面指定的GOPATH执行任何给定的命令。
//exec:执行包装在GOPATH中给定的命令,如 make exec run="go test ./..."
exec:
@GOPATH=\$(GOPATH) GOBIN=\$(GOBIN) \$(run)
虽然这个并没有很高级。我们应该使用一些常见的命令来覆盖一些常见的情况,并且只有在我们需要执行一些没有被makefile涵盖的事时,再使用exec。
开发者模式
开发者模式应该做的事:
- 清楚构建时的缓存
- 编译代码
- 后台运行服务
- 当代码有变动时,重复以上所有步骤
这些听上去很简单,但是很快就会变复杂,因为我们将同时运行服务器和文件观察器。我们需要保证再开启一个新的进程前有合理的关闭,同时也不要破坏常见的命令行行为,例如在按下ctrl+c或者ctrl+d时停止。
start:
bash -c "trap 'make stop' EXIT; $(MAKE) compile start-server watch run='make compile start-server'"
stop: stop-server
以上的代码解决了以下的一些问题:
- 在后台编译和运行服务器
- 由于主进程不在后台执行,因此我们可以使用ctrl+c来进行中断
- 当主进程被中断时,后台进程也将停止。我们需要trap也主要是因为这个。
- 代码发生改变时,重新编译并重启服务器
在以下的部分,我将详细解释这些命令。
编译
compile命令在后台处理的不仅仅是调用go compile;它也将清除错误输出并且打印简化版本。
以下是如何做到像是在命令行中做出的重大更改:
compile:
@-touch $(STDEER)
@-rm $(STDEER)
@-$(MAKE) -s go-compile 2>$(STDEER)
@cat $(STDEER) | sed -e 'ls/.*/\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
监听变化
我们需要一个文件监听器来记录变化。我尝试了很多种但仍旧不是很满意,最终还是创建了我自己的文件监听器,yolo。在系统中安装yolo:
$ go get github.com/azer/yolo
一旦安装完成,我们基本可以监听项目文件中的变化,除了verder和bin文件夹。原因是:
## watch: 当代码发生变化时运行指定命令,如 make watch run = "echo hey"
watch:
@yolo -i . -e vender -e bin -c $(run)
现在我们获取到了一个watch命令,可以递归监项目目录中的变化,当然,除了vender目录。我们可以简单地传递任何我们想要执行的命令。如,start命令在代码改变时调用make compile start-server。
make watch run = "make compile start-server"
我们可以使用它来进行测试,或者自动检查这里是否有任何竞争条件(这个不太明白…)。。执行时会设置好环境变量,因此你完全不必担心GOPATH:
make watch run = "go test ./..."
Yolo中包含的一个好东西,那就是web接口。如果你启用它,你可以马上在web接口看到你命令行的输出结果,你所需要做的只是通过-a选项去开启:
yolo -i . -e vender -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
帮助
最后,我们需要一个帮助命令来查看全部的命令。我们可以使用sed和colum命令,自动生成格式优美的输出形式。见下方:
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.
makefile最终版本
根据以上分享,这使我总结之后的最终版本。今天早上我开始了一个新项目,而这就是我从其中拷贝而来:
include .env
PROJECTNAME = $(shell basename "$PWD")
# Go 相关变量
GOBASE = $(shell pwd)
GOPATH = $(GOBASE)/vendor:$(GOBASE)
GOBIN = $(GOBASE)/bin
GOFILES = (wildcard *.go)
# 将错误输出至指定文件,在开发者模式下可以对其观察
STDERR = /tmp/.$(PROJECTNAME)-stderr.txt
# PID文件将一直一直保存服务的进程ID
PID = /tmp/.$(PROJECTNAME).pid
# make在linux中很冗杂,将其设置为slient模式
MAKEFILES += --silent
## install: 安装缺少的依赖包。在内部将执行`go get`命令。例如:make install get = github.com/foo/bar
install: go-get
## start: 在开发者模式下运行,当代码发生改变时自动给运行。
start: @bash -c "trap 'make stop ' EXIT; $(MAKE) compile start-server watch run='make compile start server'"
## stop: 停止开发者模式
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: 当代码发生改变时,运行指定命令。例如:make watch run = "echo 'hey'"
watch:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) yolo -i . -e vender -e bin -c "$(run)"
restart-server: stop-server start-server
## 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
## exec: 运行包装在GOPATH下的指定命令。例如:make exec run="go test ./..."
exec:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) $(run)
##
##
## clean: 清除构建的文件,在内部执行`go clean`
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
模板项目
我搭建了一个web服务,并使用了以上的makefile。你可以看一下这个项目,对其进行设置并可以尝试着运行。地址是:github.com/azer/go-makefile-example.
就是这样!如果你还有其他任何问题、想法或者一些建议能对这个进行优化,请email~
祝好。