提示:
- 所有体系课见专栏:Go 项目开发极速入门实战课;
- 欢迎加入 云原生 AI 实战 星球,12+ 高质量体系课、20+ 高质量实战项目助你在 AI 时代建立技术竞争力(聚焦于 Go、云原生、AI Infra);
- 本节课最终源码位于 fastgo 项目的 feature/s05 分支;
- 更详细的课程版本见:Go 项目开发中级实战课:14 | 应用构建(3):如何给应用添加版本号打印功能?
在 Go 项目开发中,为了方便排障等,需要知道某个线上应用的具体版本。另外,在你开发命令行工具的时候,也需要支持 -v/--version
之类的命令行参数。这时候,就需要给应用添加版本号打印功能。
本节课,就来看下,如何给应用添加版本号打印功能。
如何添加版本号?
在实际开发中,当完成一个应用特性开发后,会编译应用源码并发布到生产环境。为了定位问题或出于安全考虑(确认发布的是正确的版本),开发者通常需要了解当前应用的版本信息以及一些编译时的详细信息,例如编译时使用的 Go 版本、Git 目录是否干净,以及基于哪个 Git 提交 ID 进行的编译。在一个编译好的可执行程序中,通常可以通过类似。/appname -v
的方式来获取版本信息。
我们可以将这些信息写入版本号配置文件中,程序运行时从版本号配置文件中读取并显示。然而,在程序部署时,除了二进制文件外还需要额外的版本号配置文件,这种方式既不方便,又面临版本号配置文件被篡改的风险。另一种方式是将这些信息直接写入代码中,这样无需额外的版本号配置文件,但每次编译时都需要修改代码以更新版本号,这种实现方式同样不够优雅。
Go 官方提供了一种更优的方式:通过编译时指定 -ldflags -X importpath.name=value
参数,来为程序自动注入版本信息。
提示:在实际开发中,绝大多数情况是使用 Git 进行源码版本管理,因此 fastgo 的版本功能也基于 Git 实现。
给 fg-apisearver 组件添加版本号功能
可以通过以下步骤为 fg-apiserver 添加版本功能:
- 创建一个 version 包用于保存版本信息;
- 将版本信息注入到 version 包中;
- fg-apiserver 应用添加
--version
命令行选项。
创建一个 version 包
创建一个 pkg/version/version.go 文件,代码如下所示:
package versionimport ("encoding/json""fmt""runtime""github.com/gosuri/uitable"
)var (// semantic version, derived by build scripts (see// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/release/versioning.md// for a detailed discussion of this field)//// TODO: This field is still called "gitVersion" for legacy// reasons. For prerelease versions, the build metadata on the// semantic version is a git hash, but the version itself is no// longer the direct output of "git describe", but a slight// translation to be semver compliant.// NOTE: The $Format strings are replaced during 'git archive' thanks to the// companion .gitattributes file containing 'export-subst' in this same// directory. See also https://git-scm.com/docs/gitattributesgitVersion = "v0.0.0-master+$Format:%H$"gitCommit = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD)gitTreeState = "" // state of git tree, either "clean" or "dirty"buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ')
)// Info contains versioning information.
type Info struct {GitVersion string `json:"gitVersion"`GitCommit string `json:"gitCommit"`GitTreeState string `json:"gitTreeState"`BuildDate string `json:"buildDate"`GoVersion string `json:"goVersion"`Compiler string `json:"compiler"`Platform string `json:"platform"`
}// String returns info as a human-friendly version string.
func (info Info) String() string {return info.GitVersion
}// ToJSON returns the JSON string of version information.
func (info Info) ToJSON() string {s, _ := json.Marshal(info)return string(s)
}// Text encodes the version information into UTF-8-encoded text and
// returns the result.
func (info Info) Text() string {table := uitable.New()table.RightAlign(0)table.MaxColWidth = 80table.Separator = " "table.AddRow("gitVersion:", info.GitVersion)table.AddRow("gitCommit:", info.GitCommit)table.AddRow("gitTreeState:", info.GitTreeState)table.AddRow("buildDate:", info.BuildDate)table.AddRow("goVersion:", info.GoVersion)table.AddRow("compiler:", info.Compiler)table.AddRow("platform:", info.Platform)return table.String()
}// Get returns the overall codebase version. It's for detecting
// what code a binary was built from.
func Get() Info {// These variables typically come from -ldflags settings and in// their absence fallback to the settings in pkg/version/base.goreturn Info{GitVersion: gitVersion,GitCommit: gitCommit,GitTreeState: gitTreeState,BuildDate: buildDate,GoVersion: runtime.Version(),Compiler: runtime.Compiler,Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),}
}
version 包用于记录版本号信息,而版本号功能几乎是所有 Go 应用都会用到的通用功能。因此,需要考虑将 version 包提供给其他外部应用程序使用。根据目录规范,应将 version 包放在 pkg/ 目录下,以便其他项目可以导入并使用 version 包。由于 version 包需要是面向第三方应用的包,因此需确保 version 包的功能稳定、完善,并能够独立对外提供预期的功能。
上述代码, 定义了一个 Info 结构体,用于统一保存版本信息。Info
结构体记录了较为详细的构建信息,包括 Git 版本号、Git 提交 ID、Git 仓库状态、应用构建时间、Go 版本、用到的编译器和构建平台。此外,Info
结构体还实现了以下方法,用于展示不同格式的版本信息:
- Get 方法:返回详尽的代码库版本信息;
- String 方法:以更友好、可读的格式展示构建信息;
- ToJSON 方法:以 JSON 格式输出版本信息;
- Text 方法:展示格式化的版本信息。
将版本信息注入到 version 包中
接下来,可以通过 -ldflags -X "importpath.name=value"
构建参数将版本信息注入到 version 包中。
由于需要解析当前 Git 仓库的状态、Commit ID、Tag 等信息,为了方便在编译时将版本号信息注入到 version 包中,这里我们将这些注入操作统一封装到 build.sh 脚本中。这样,在编译 fg-apiserver 组件时,只需要执行 build.sh 脚本即可。
build.sh 脚本内容如下:
#!/bin/bash# 获取脚本所在目录作为项目根目录
PROJ_ROOT_DIR=$(dirname "${BASH_SOURCE[0]}")# 定义构建产物的输出目录为项目根目录下的_output文件夹
OUTPUT_DIR=${PROJ_ROOT_DIR}/_output# 指定版本信息包的路径,后续会通过-ldflags参数将版本信息注入到这个包的变量中
VERSION_PACKAGE=github.com/onexstack/fastgo/pkg/version# 确定VERSION值:如果环境变量中没有设置VERSION,则使用git标签作为版本号
# git describe --tags --always --match='v*'命令会获取最近的v开头的标签,如果没有则使用当前commit的短哈希
if [[ -z "${VERSION}" ]];thenVERSION=$(git describe --tags --always --match='v*')
fi# 检查代码仓库状态:判断工作目录是否干净
# 默认状态设为"dirty"(有未提交更改)
GIT_TREE_STATE="dirty"
# 使用git status检查是否有未提交的更改
is_clean=$(git status --porcelain 2>/dev/null)
# 如果is_clean为空,说明没有未提交的更改,状态设为"clean"
if [[ -z ${is_clean} ]];thenGIT_TREE_STATE="clean"
fi# 获取当前git commit的完整哈希值
GIT_COMMIT=$(git rev-parse HEAD)# 构造链接器标志(ldflags)
# 通过-X选项向VERSION_PACKAGE包中注入以下变量的值:
# - gitVersion: 版本号
# - gitCommit: 构建时的commit哈希
# - gitTreeState: 代码仓库状态(clean或dirty)
# - buildDate: 构建日期和时间(UTC格式)
GO_LDFLAGS="-X ${VERSION_PACKAGE}.gitVersion=${VERSION} \-X ${VERSION_PACKAGE}.gitCommit=${GIT_COMMIT} \-X ${VERSION_PACKAGE}.gitTreeState=${GIT_TREE_STATE} \-X ${VERSION_PACKAGE}.buildDate=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"# 执行Go构建命令
# -v: 显示详细编译信息
# -ldflags: 传入上面定义的链接器标志
# -o: 指定输出文件路径和名称
# 最后参数是入口文件路径
go build -v -ldflags "${GO_LDFLAGS}" -o ${OUTPUT_DIR}/fg-apiserver -v cmd/fg-apiserver/main.go
脚本中已有详尽的注释,这里不再多做说明。
fg-apiserver 添加 --version
命令行选项
通过前面的步骤,在编译 fg-apiserver 之后,所需的版本信息已成功注入 version 包中。接下来,还需要在 fg-apiserver 主程序中调用 version 包打印版本信息。
编辑 cmd/fg-apiserver/app/server.go 文件,在 run 函数中添加以下代码:
package appimport (..."github.com/onexstack/fastgo/pkg/version"
)
...
// NewFastGOCommand 创建一个 *cobra.Command 对象,用于启动应用程序.
func NewFastGOCommand() *cobra.Command {...// 添加 --version 标志version.AddFlags(cmd.PersistentFlags())return cmd
}// run 是主运行逻辑,负责初始化日志、解析配置、校验选项并启动服务器。
func run(opts *options.ServerOptions) error {// 如果传入 --version,则打印版本信息并退出version.PrintAndExitIfRequested()...
}
version.AddFlags(cmd.PersistentFlags())
用来给 fg-apiserver 命令添加 -v/--version
命令行选项。
version.PrintAndExitIfRequested()
用来指定当 fg-apiserver 命令执行并传入 -v/--version
命令行选项时,应用会打印版本号信息并退出。
测试 fg-apiserver 版本号打印功能
开发完成后,执行以下命令来编译 fg-apiserver 组件源码,并打印版本号信息:
$ git tag -a v0.0.1 -m "release v0.0.1" # 给当前 Commit 打上标签
$ ./build.sh # 编译 fg-apiserver 源码
$ _output/fg-apiserver --version # 打印版本号信息
v0.0.1
$ _output/fg-apiserver --version=raw # 打印更详细的编译信息,包括版本号gitVersion: v0.0.1 gitCommit: b9d5b4425c8296b2c7642a35589ae83b6f7a8721
gitTreeState: clean buildDate: 2025-03-03T04:52:53Z goVersion: go1.24.0 compiler: gc platform: linux/amd64
可以看到,fg-apiserver 程序根据 --version
的值输出了不同格式且内容详尽的版本信息。通过这些版本信息,可以精确定位当前应用所使用的代码及编译环境,为日后的故障排查奠定了坚实的基础。