最近看到的一篇文章,@Journal 分享的关于Golang Web程序Makefile的实践。感觉会经常用到,这里做下记录。

针对Go构建Web程序,Makefile需要达到以下功能:

  • 命令行高级而简单,例如compile, start, stop, watch等。
  • 能够管理项目特定的环境变量。所以它应该包含一个 .env 文件。
  • 开发模式下,当有变更能够自动编译。
  • 开发模式下,能够自动重启程序
  • 项目指定 GOPATH ,这样我们就可以将依赖放入 vendor 目录
  • 可以指定文件监控,例如 make watch run="go test ./..."

典型的目录结构如下:

1
2
3
4
5
6
.env
Makefile
main.go
bin/
src/
vendor/

在此文件结构下输入 make 命令将提供以下输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$  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.

1. 实现步骤

环境变量

首先我们要在Makefile文件中包含我们为项目定义的环境变量,那么我们的第一行就是:

1
include .env

在环境变量中我们会定义很多,比如项目名称,Go文件夹/文件,pid文件路径等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 store the server process id when it's running on development mode
PID=/tmp/.$(PROJECTNAME)-api-server.pid

# Make is verbose in Linux. Make it silent.
MAKEFLAGS += --silent

在Makefile的其他部分,我们将会使用到 GOPATH 变量。我们所有的命令都应该包含在项目制定的 GOPATH 中,否则它们就无法生效。这样我们可以隔离每个项目,但是也带来了一些复杂度。为了简化操作,我们可以添加一个 exec 命令,该命令用来执行指定的命令,并使用上面定义的 GOPATH

1
2
3
## exec: Run given command, wrapped with custom GOPATH. e.g; make exec run="go test ./..."
exec:
	@GOPATH=$(GOPATH) GOBIN=$(GOBIN) $(run)

但是这还不够高级,我们应该用一些简单的命令来做一些常见的事情。如果我们要做Makefile还未涵盖的事情,那就只能使用 exec

开发者模式

开发者模式应该实现以下功能:

  • 清理build缓存
  • 编译代码
  • 后台运行项目程序
  • 当代码有变更时,自动执行上面的功能

这个听起来很简单,但是很快就会变得很复杂。因为我们要同时运行web程序和文件监听,我们需要确保在启动新进程之前旧的进程正确停止,并且不能破坏常见的命令行行为,例如在按下Control-C或者Control-D时程序停止运行。

1
2
3
4
start:
	bash -c "trap 'make stop' EXIT; $(MAKE) compile start-server watch run='make compile start-server'"

stop: stop-server

以下是代码需要解决的问题:

  • 编译并在后台运行web server程序
  • 主进程不会在后台运行,这样我们就可以在我们需要的时候使用Control-C
  • 当主进程被关闭的时候,后台服务进程也需要停止。
  • 当代码有变更的时候,重新编译程序并重启server

在下面的部分,我们会详细的解释这些命令。

编译

compile命令并不仅仅是要在后台调用go compile,它还要清理错误输出,并打印简化版本。

1
2
3
4
5
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

启动/停止 Server

start-server 就是运行它在后台编译的二进制文件,同时要保存PID到临时文件中。

stop-server 就是在需要的时候读取PID,并杀掉进程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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,你可以把它安装到你的系统中:

1
$  go get github.com/azer/yolo

一旦安装完毕,我们基本就可以开始监控项目目录中的变更,当然会排除vendorbin目录。

1
2
3
## watch: Run given command when code changes. e.g; make watch run="echo 'hey'"
watch:
	@yolo -i . -e vendor -e bin -c $(run)

现在我们有了 watch 命令,它会在项目目录中监控代码变更。我们可以传递任何我们想要运行的命令,例如在start命令,当有代码变更时运行make compile start-server:

1
make watch run="make compile start-server"

我们可以用它来运行测试,或者自动检查数据竞争。在运行时环境变量会被自动设置,你所以我们不需要担心GOPATH

1
make watch run="go test ./..."

使用Yolo的还有一个好的特性就是它有网络界面。如果弃用它,我们就可以在Web见面中看到命令的输出。而您只需要传递-a选项就可以启动它。

1
yolo -i . -e vendor -e bin -c "go run foobar.go" -a localhost:9001

然后你就可以在浏览器中打开localhost:9001,并实时看到结果。

文件监控

安装依赖

在我们修改代码是,我们希望在编译之前自动下载缺少的依赖项,install命令可以帮助我们做这些工作。

1
install: go-get

我们会在文件变更,编译之前自动调用install命令,这样就能够自动安装依赖项了。如果你想手动安装依赖,你可以运行:

1
make install get="github.com/foo/bar"

实际上,这个命令会被转换为:

1
$ GOPATH=~/my-web-server GOBIN=~/my-web-server/bin go get github.com/foo/bar

Go命令

因为我们希望将GOPATH设置为项目目录,以简化依赖关系管理,所以我们需要把所有的Go命令包装在Makefile内。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 命令来查看可用命令的介绍。我们可以使用sedcolumn命令来自动生成格式化好的帮助输出。如下:

1
2
3
help: Makefile
	@echo " Choose a command run in "$(PROJECTNAME)":"
	@sed -n 's/^##//p' $< | column -t -s ':' |  sed -e 's/^/ /'

上面的命令会扫描Makefile中##开头的行,并输出它们。因此我们可以简单的注释一下定义的命令,这些注释将会在help命令中发挥作用。

1
2
3
4
5
6
7
8
## 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

我们运行help命令就可以得到下面这样的输出:

1
2
3
4
5
6
7
 $  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.

2. 最终版本

下面就是这个Makefile的最终版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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) clean compile start-server watch run='make clean 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:
	@-rm $(GOBIN)/$(PROJECTNAME) 2> /dev/null
	@-$(MAKE) go-clean

go-compile: 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/^/ /'

3. 项目示例

我使用上面的Makefile创建了一个web server示例,你可以照着示例来尝试一下。

地址: https://github.com/azer/go-makefile-example

4. 原文地址

原文地址:A Good Makefile for Go - A refined Makefile to simplify building and managing web servers written in Go