编译 Go 语言程序时注入版本信息

在定位线上应用程序的问题的时候,确定应用程序使用的版本号等细节通常是非常有必要做的且非常有帮助的。 于是我们通常会在最终生成的二进制文件中嵌入相关信息,比如:编译时间、谁编译的、当前分支、当前 Tag、当前的提交号等。 这篇文章讲解在 Go 语言程序中注入版本信息的方式。

链接器参数

Go 语言的链接器文档中提到了一个 -X 参数,用来修改一个包的字符串变量的值。 官方说明如下:

-X importpath.name=value
	Set the value of the string variable in importpath named name to value.
	This is only effective if the variable is declared in the source code either uninitialized
	or initialized to a constant string expression. -X will not work if the initializer makes
	a function call or refers to other variables.
	Note that before Go 1.5 this option took two separate arguments.

有几个重点:

  • 变量的类型需要是字符串类型;
  • 变量未被初始化,或被初始化成一个字符串常量表达式;
  • 如果被初始化成一个函数调用的返回值,或是引用其它变量,则不会生效;
  • 变量不需要是导出变量(文档未提及);
  • 可以多次使用该参数以修改多个变量的值;
  • 不存在的变量会直接忽略。

然而,我们通常直接使用 go build 一个命令来完成编译与链接工作,不会直接调用链接器。 所以,应该让 go build 帮我们把这些参数传递给链接器。这时候需要用到 go build-ldflags 参数:

-ldflags '[pattern=]arg list'
        arguments to pass on each go tool link invocation.

特别需要注意 shell 语法中引号、空格的使用,不然很容易出错。

修改 main 包的变量

package main

import (
	"fmt"
)

var (
	builtAt string
)

func main() {
	fmt.Printf("Built At: %s\n", builtAt)
}

编译及运行:

$ go build -ldflags "-X 'main.builtAt=$(date)'"
$ ./main
Built At: Thu Jun  4 23:35:02 CST 2020

这个结果不是很好看,可以格式化一下 date 的输出:

$ go build -ldflags "-X 'main.builtAt=$(date +'%F %T %z')'"
$ ./main
Built At: 2020-06-04 23:37:43 +0800

修改导入包的变量

导入包需要写完整的导入路径。

比如我的 main 导入了一个外部包:

package main

import (
	"fmt"

	"github.com/movsb/version"
)

func main() {
	fmt.Printf("Built At: %s\n", version.BuiltAt)
}

可以用下面的命令修改 version 包的 BuiltAt 变量的值:

$ go build -ldflags "-X 'github.com/movsb/version.BuiltAt=$(date)'"

因为我要在 main 包使用这个变量,所以将其大写后导出了。

同时修改多个变量

用空格把它们分开就好了,注意引号把全部引起来。

package main

import (
	"fmt"
)

var (
	builtOn string
	builtAt string
)

func main() {
	fmt.Printf(""+
		"Built On: %s\n"+
		"Built At: %s\n",
		builtOn, builtAt,
	)
}

编译命令及运行结果:

$ go build -ldflags "-X 'main.builtOn=$USER@$(hostname)' -X 'main.builtAt=$(date +'%F %T %z')'"
$ ./main
Built On: tao@MacBookPro-Home.local
Built At: 2020-06-04 23:49:25 +0800

在脚本中执行

手写太长的命令通常不是一个好注意,放在脚本中是个不错的选择。

以下代码片段节选中我博客的相关代码

  • main.go

    package main
      
    import (
      "fmt"
      "os"
    )
      
    var (
      builtOn   string
      builtAt   string
      goVersion string
      gitAuthor string
      gitCommit string
    )
      
    func main() {
      fmt.Fprintf(os.Stderr, ""+
          "Built On  : %s\n"+
          "Built At  : %s\n"+
          "Go Version: %s\n"+
          "Git Author: %s\n"+
          "Git Commit: %s\n",
          builtOn, builtAt,
          goVersion, gitAuthor, gitCommit,
      )
    }
    
  • build.sh

    #!/bin/bash
      
    set -euo pipefail
      
    builtOn="$USER@$(hostname)"
    builtAt="$(date +'%F %T %z')"
    goVersion=$(go version | sed 's/go version //')
    gitAuthor=$(git show -s --format='format:%aN <%ae>' HEAD)
    gitCommit=$(git rev-parse HEAD)
      
    ldflags="\
    -X 'main.builtOn=$builtOn' \
    -X 'main.builtAt=$builtAt' \
    -X 'main.goVersion=$goVersion' \
    -X 'main.gitAuthor=$gitAuthor' \
    -X 'main.gitCommit=$gitCommit' \
    "
      
    go build -ldflags "$ldflags" 
    

编译及运行结果:

$ ./build.sh 
$ ./main 
Built On  : tao@MacBookPro-Home.local
Built At  : 2020-06-04 23:57:45 +0800
Go Version: go1.13.4 darwin/amd64
Git Author: me <tao@example.com>
Git Commit: 64ea5e7e9e8f15f1ca07c23f5e8894db08295e29

参考:

发表于:2020年6月5日 ,阅读量:121 ,标签:Go