我最喜欢 Go 的一点就是它使用 go install
[1] 的分发过程。使用 go install
,我不需要像其他语言那样单独设置 brew
、npm
、pip
或任何其他包管理器。它开箱即用!
之前 我用 Go 构建了一个命令行(CLI)应用[2] 。我想为用户添加一个简单的方法,让他们在通过 go install
安装应用后可以检查应用版本。这让我发现了在 Go 中添加版本标志的不同方法。
我想要实现的目标
假设我们有一个名为 mym
的 Go CLI 应用。我的目标很简单:
- 用户通过
go install
安装应用后就能直接使用 - 我希望用户输入
mym -version
或mym -v
就能看到应用的当前版本:
❯ go install github.com/ngshiheng/michelin-my-maps/v2/cmd/mym@v2.6.1 ❯ mym --version Version: v2.6.1
这让我探索了在 Go 中实现这一目标的不同方法。以下是我的发现:
方法1: 构建时注入
如果你在网上搜索过,你可能已经看到这个常见的解决方案。它涉及在构建过程中使用 -ldflags
开关将版本信息设置到二进制文件中。以下是具体步骤:
步骤1: 定义版本变量
首先,在 main
包中定义一个版本变量:
// Version is set during build time using -ldflags var Version = "unknown" // printVersion prints the application version func printVersion() { fmt.Printf("Version: %s\n", Version) } func main() { versionFlag := flag.Bool("version", false, "print version information") flag.Parse() if *versionFlag { printVersion() return } // ... }
步骤2: 使用版本标志构建
然后,在 go build
命令中添加 -ldflags
标志来动态设置版本:
❯ go build -ldflags "-X 'main.Version=v2.6.0'" cmd/mym/mym.go ❯ ./mym --version Version: v2.6.0
步骤3: 在某处托管二进制文件
一旦你使用 -ldflags
构建了带版本标记的 Go 二进制文件,下一步就是将二进制文件托管在用户可以下载的平台上,例如 GitHub release、AWS S3 或你自己的服务器。
下载这个二进制文件的用户就可以运行命令并获得相同的版本信息:
# (... download binary directly) ❯ mym --version Version: v2.6.0
缺点
虽然这种方法可行,但我认为它对开发者和用户都有一些主要缺点:
- 需要额外的步骤
- 你不能期望用户自己用特定的
ldflag
构建应用 - 用户不能在
go install
时传递ldflags
(即使可以,期望用户使用一个又长又复杂的命令来安装你的应用也是糟糕的用户体验)
CI/CD 呢?
问题在于:我已经有了 处理自动发布的 CI[3] 。实现这种方法需要:
- 设置另一个工作流,或者
- 修改现有工作流以包含使用版本标志构建和上传二进制文件的步骤。
这增加了构建过程的复杂性。我认为我们可以做得更好!
方法2: 从运行时构建信息读取
Git 标签在 Go 模块的发布和版本控制[4] 中起着重要作用。
Git 中的每个版本标签对应模块的特定发布版本。当你将新标签推送到仓库时,它会创建模块的新版本。
然后我想到:
既然我的 Go CLI 应用是一个 Go 模块,版本信息已经在 Git 标签中了,对吧?为什么不通过在运行时读取它来重用呢?
以下是如何实现这种方法
import ( "fmt" "runtime/debug" ) // printVersion prints the application version func printVersion() { buildInfo, ok := debug.ReadBuildInfo() if !ok { fmt.Println("Unable to determine version information.") return } if buildInfo.Main.Version != "" { fmt.Printf("Version: %s\n", buildInfo.Main.Version) } else { fmt.Println("Version: unknown") } } func main() { versionFlag := flag.Bool("version", false, "print version information") flag.Parse() if *versionFlag { printVersion() os.Exit(0) } // ... }
从最终用户的角度来看,他们只需要做:
❯ go install github.com/ngshiheng/michelin-my-maps/v2/cmd/mym@v2.6.1 ❯ mym --version Version: v2.6.1
这意味着我不需要修改现有的 CI/CD 工作流,而且可以从我们的自动发布(git tag
)中获得开箱即用的版本控制!
方法3: 使用 versioninfo
模块
在我写这篇文章时,我看到了 这篇博客文章[5] ,其中有人遇到了相同的问题并 为此创建了一个 Go 包[6] 。这个简单的包允许你只用两行代码就为你的 CLI 添加版本标志!
package main import ( "flag" "fmt" "github.com/carlmjohnson/versioninfo" // 1. import ) func main() { versioninfo.AddFlag(nil) // 2. add flag flag.Parse() }
但是,如果你真的不想在已经很长的依赖列表中再添加另一个模块,你可能应该坚持使用方法2。
总结
为 Go CLI/应用添加版本信息比我最初想象的更有趣。
虽然这三个选项都是有效的,但方法2最符合我的需求。它不需要任何特殊的构建步骤,可以与 go install
一起使用。作为额外的优点,它不依赖任何其他包,而且不会让你在 包消失[7] 时手忙脚乱。
参考链接
go install
: https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies- 我用 Go 构建了一个命令行(CLI)应用: https://jerrynsh.com/how-i-scraped-michelin-guide-using-golang/
- 处理自动发布的 CI: https://github.com/ngshiheng/michelin-my-maps/blob/3dadd302e607efd6ad31a95922f316316bf8fe69/.github/workflows/release.yml#L17
- Go 模块的发布和版本控制: https://go.dev/blog/publishing-go-modules
- 这篇博客文章: https://blog.carlana.net/post/2023/golang-git-hash-how-to/
- 为此创建了一个 Go 包: https://github.com/earthboundkid/versioninfo
- 包消失: https://en.wikipedia.org/wiki/Npm_left-pad_incident