MENU

利用动态信息注入,优雅的实现Go程序构建

October 30, 2022 • Read: 1156 • Linux,编码,Go

使用 Docker 或者 k8s 工具类软件,通过 version 命令查看版本号后,我们会发现除了基础的版本号之外还会有很多其他的信息:

[dbkuaizi@MiWiFi-RA69-srv ~]# docker version
Client:
 Version:         1.13.1
 API version:     1.26
 Package version: docker-1.13.1-209.git7d71120.el7.centos.x86_64
 Go version:      go1.10.3
 Git commit:      7d71120/1.13.1
 Built:           Wed Mar  2 15:25:43 2022
 OS/Arch:         linux/amd64

Server:
 Version:         1.13.1
 API version:     1.26 (minimum version 1.12)
 Package version: docker-1.13.1-209.git7d71120.el7.centos.x86_64
 Go version:      go1.10.3
 Git commit:      7d71120/1.13.1
 Built:           Wed Mar  2 15:25:43 2022
 OS/Arch:         linux/amd64
 Experimental:    false

在用户向开发者反馈问题时,通过这些信息可以辅助我们更快地定位到用户正在使用的版本信息。尤其是在一些特殊场景下我们甚至会对同一个版本号编译多个程序时,编译时间和 commit id 会显得尤为重要。

方案选择

如果要实 现上面的效果,我们有两种做法:

硬编码
将上面的信息通过硬编码的方式,直接写死在程序的逻辑代码中。这样的方式弊端也很明显,每次构建都需要修改代码,修改后的代码又无法与 git log 中提交的代码保持一致。

还有一点就是:让程序员去做这种重复性的工作,自然也是不可能的,所以我们需要通过更优雅的方式来实现这个功能。

动态注入

如果不想每次构建时都修改代码,我们只能在构建的时候做一些手脚。

在执行 go build 时有一个可选参数 -ldflags,作用是可以通过动态注入的方式给预定义的变量设置默认值(如果你在定义变量的时候给了一个默认值,那么在动态注入的时候会被注入的值覆盖掉)。需要注意的一点是这种方式值只可以设置 string 类型的字符串。

小试牛刀

先编写代码,获取注入的值

package main

import "fmt"

// 定义三个变量 用于接收动态注入的值
var version, buildTime, goVersion string

func main() {
    // 输出动态注入的值
    fmt.Printf("Version   : %s\n", version)
    fmt.Printf("Build Time: %s\n", buildTime)
    fmt.Printf("GoVersion : %s\n", goVersion)
}

然后在 go rungo build 时,通过 -ldflags 注入:

$ go run -ldflags "-X 'main.version=1.0.0' -X 'main.buildTime=2022-10-29 23:40' -X 'main.goVersion=1.19.1'" ./main.go 
Version   : 1.0.0
Build Time: 2022-10-29 23:40
GoVersion : 1.19.1

$ go build -ldflags "-X 'main.version=1.0.0' -X 'main.buildTime=2022-10-29 23:25' -X 'main.goVersion=1.19.1'" ./main.go
$ ./main.exe
Version   : 1.0.0
Build Time: 2022-10-29 23:25
GoVersion : 1.19.1

再进一步

经过上面的尝试,我们已经实现了想要的效果了,只需要在每次构建的时候手动传入七八个参数就可以了,除了有点废人,看起来没有别的毛病了。

正如上面方案选择时所说,作为程序员,我们需要更优雅的方式来进行构建。

编写 Version 输出逻辑的代码:
在项目的 common 包中新建 version.go 文件,并编写如下代码:

package common

import (
    "fmt"
    "os"
    "runtime"
)

var (
    Version   string // 版本号
    GoVersion string // Go 版本信息
    BuildTime string // 构建时间
    GitCommit string // git commit id
    GitBranch string // git 分支名称
)

func InitVersion() {
    args := os.Args
    if len(args) == 2 && args[1] == "version" {
        fmt.Printf("Version        => %s\n", Version)
        fmt.Printf("Go Version    => %s\n", GoVersion)
        fmt.Printf("Git Commit    => %s/%s\n", GitCommit, GitBranch)
        fmt.Printf("build Time    => %s\n", BuildTime)
        fmt.Printf("Compiler    => %s\n", runtime.Compiler)
        fmt.Printf("Platform    => %s\n", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH))
        // 输出版本信息后直接结束程序,防止与已运行的程序冲突。
        os.Exit(0)
    }
}

在 main 程序中引入:

package main

import "ginweb/common"

func main() {
    common.InitVersion()
}

注意:如程序不允许重复运行多个,则一定要在程序的初始化之前执行版本号输出的逻辑,防止与已运行的服务冲突。

编写构建脚本:

#!/bin/bash
# 判断是否有输入版本号
if [ "$1" != "" ]; then Version=$1;else
    read -p "Input Build Version: " Version; if [ "$Version" = "" ]; then echo "The input cannot be empty";exit;fi
fi

echo "Build Start  " $(date "+%F %T.")$((`date +%N`/1000000))
# 获取 go.mod 项目名,用来指定注入变量位置及输出可以执行程序名称
ModuleName=`head go.mod | grep "^module" | awk '{print $2}'`
# 获取构建时间
BuildTime=$(date "+%F %T")
# 获取构建时 Go 环境信息
GoVersion=`go version`
# 获取构建时 Commit ID
GitCommit=`git rev-parse HEAD`
# 获取构建时的 Git 分支
GitBranch=`git rev-parse --abbrev-ref HEAD`
go build -ldflags="-s -w
      -X '${ModuleName}/common.Version=${Version}'
      -X '${ModuleName}/common.GoVersion=${GoVersion}'
      -X '${ModuleName}/common.GitCommit=${GitCommit}'
      -X '${ModuleName}/common.GitBranch=${GitBranch}'
      -X '${ModuleName}/common.BuildTime=${BuildTime}'" -o $ModuleName main.go
echo -e "Build Success" $(date "+%F %T.")$((`date +%N`/1000000))

最终成果

# 直接输入版本号
$ ./build.sh v1.2.0
Build Start   2022-10-30 02:08:12.949
Build Success 2022-10-30 02:08:14.221

# 询问时输入版本号
$ ./build.sh
Input Build Version: v1.2.0
Build Start   2022-10-30 02:09:04.541
Build Success 2022-10-30 02:09:05.867

# 执行程序输出内容
Version         => v1.2.0
Go Version      => go version go1.19.1 windows/amd64
Git Commit      => b1b45eecf1c27acc00a9455e908cf19e1453759b/main
build Time      => 2022-10-30 02:09:04
Compiler        => gc
Platform        => windows/amd64

效果还不错,阿尼亚看了估计都得说一声优雅。
优雅.jpg

Windows 构建

如果你的编码环境是 Windows,那么可以通过 git bash 等模拟终端执行 shell 文件来构建程序。
注意:

  • 考虑到绝大多数打包环境和构建环境为 Linux 所以 build 输出文件名没有设置具体的后缀。可在输出后修改后缀为 .exe,或者在 shell 脚本中修改为 -o $ModuleName.exe
  • 也可以增加 shell 脚本的逻辑,判断不同的构建平台,来决定是否添加 exe 后缀。

更多尝试

本文中的 Shell 只是很基础的一些打包功能,实际使用中可以根据自己的不同需求进一步的修改 Shell 脚本,例如这几个思路:

  • Shell 脚本中增加跨系统、跨架构的构建功能(例如 一次性构建 mac、linux、windows 等多个平台的可执行程序)。
  • 在构建完成之后,调用 upx 对构建后的程序进行压缩,以减小程序体积。
Last Modified: January 27, 2023