Go,Makefile与自动程序版本号的实现

在发布和部署程序时,我们往往会有这样的需求:把版本号内置在程序里面,运行和部署程序的时候,可以用来知晓当前发布和部署的程序是什么版本。在一个编译好的可执行程序中,我们通常可以用类似: ./app_name -version 的方式,来获取当前程序的版本号。有了程序的版本号,更便于生产环境中,当程序出现问题时,工程师可以方便的根据版本号查找对应代码的改动,从而更容易定位到问题的所在。

这里主要介绍一下如何用Makefile以及Go本身所支持的编译特性,实现编译时自动生成版本号的功能。

在开始之前,假设我们程序发布的流程是这样:

  • 编码完成,提交测试工程师测试
  • 测试工程师测试出的bug都已解决,并重新测试和验证通过
  • 工程师把修复好bug的代码合并到release分支,并建立git tag:2016.06.11.release ,准备用于最终程序的构建,发布和部署

在编译生成最终要发布的程序时,我们希望程序可以通过 -v 参数,提供如下的版本信息:

  • BuildTime: 显示程序编译和构建时的日期
  • GitTag: 显示程序当前代码的git tag,用以在程序出现问题时,查找和定位对应版本的源代码

首先,我们需要实现一个demo程序,用于接收 -v 参数,在运行的时候,显示当前程序的编译构建时间,以及代码编译时所使用的git tag名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"flag"
"fmt"
)

var (
GitTag = "2000.01.01.release"
BuildTime = "2000-01-01T00:00:00+0800"
)

func main() {
version := flag.Bool("v", false, "version")
flag.Parse()

if *version {
fmt.Println("Git Tag: " + GitTag)
fmt.Println("Build Time: " + BuildTime)
}
}

在程序中,我们为git tag和build time定义了默认值。运行起来,是这样的:

1
2
3
./main -v
Git Tag: 2000.01.01.release
Build Time: 2000-01-01T00:00:00+0800

我们已经有了一个程序,接收 -v 参数,并且能输出程序对应的git tag和构建时间。不过不足的地方在于,每一次发布程序的时候,我们需要手动修改这两个值,再进行编译,非常不人性化,而且在发布程序的时候,总会忘掉这一步。于是,得用上Go的link tool所提供的一个功能: -X 参数:

1
2
3
4
-X importpath.name=value
Set the value of the string variable in importpath named name to value.
Note that before Go 1.5 this option took two separate arguments.
Now it takes one argument split on the first = sign.

使用-X参数,允许我们在编译构建Go程序的时候,传入自定义的值,覆盖对应的import path下的指定变量。于是,我们可以在编译程序的时候这么干:

1
go build -ldflags "-X main.GitTag=2016.06.11.release -X main.BuildTime=2016-06-11T16:11:49+0800" main.go

编译成功后,运行 ./main -v 程序输出:

1
2
3
./main -v
Git Tag:2016.06.11.release
Build Time:2016-06-11T16:11:49+0800

到目前为止,我们已经成功的实现了”Compile time variables”,这意味着在编译时,动态传入参数和值,让程序在编译的时候,动态生成我们所指定的版本号。

不过,这个git tag和build time是在我们编译的时候指定的,而且编译的命令很长,作为一个懒惰的程序员,每次编译的时候敲这么长的命令是很磨人的。得用一种更灵活的方法来实现编译时传入参数,可选的方式大概有两种:

  • 写一个shell脚本来实现编译的自动化
  • 写一个Makefile文件,用make命令来编译

shell脚本是一种比较常用的方式,这次我们来体验一下用Makefile实现Go程序的编译。于是,我们又折腾出来了一个Makefile文件:

1
2
3
4
5
6
7
8
9
10
11
12
# This is how we want to name the binary output
OUTPUT=main

# These are the values we want to pass for Version and BuildTime
GITTAG=$(tag)
BUILD_TIME=`date +%FT%T%z`

# Setup the -ldflags option for go build here, interpolate the variable values
LDFLAGS=-ldflags "-X main.GitTag=${GITTAG} -X main.BuildTime=${BUILD_TIME}"

all:
go build ${LDFLAGS} -o ${OUTPUT} main.go

最后,我们直接用make命令传递参数,来编译我们的程序:

1
make all tag=2016.06.11.release

生成main可执行文件,然后运行查看结果:

1
2
3
./main -v
Git Tag: 2016.06.11.release
Build Time: 2016-06-13T14:50:12+0800

程序输出了我们传入的git tag,以及当时编译的时间。

最后的优化:git tag现在还是我们手动输入的,其实我们可以通过git命令 git describe –tags 来获取当前代码的tag,我们的Makefile最终可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
# This is how we want to name the binary output
OUTPUT=main

# These are the values we want to pass for Version and BuildTime
GITTAG=`git describe --tags`
BUILD_TIME=`date +%FT%T%z`

# Setup the -ldflags option for go build here, interpolate the variable values
LDFLAGS=-ldflags "-X main.GitTag=${GITTAG} -X main.BuildTime=${BUILD_TIME}"

all:
go build ${LDFLAGS} -o ${OUTPUT} main.go

然后,每次在程序部署的时候,通过git tag取代码,直接 make all就完事儿了……

支持原创技术分享,据说打赏我的人,都找到了女朋友!