- go 版本管理(g)源码阅读笔记(一个 go 版本管理工具)
- 原文作者:suhanyujie
- 文章来自:github.com/suhanyujie/…
- ps:水平有限,如有不当之处,欢迎指正。
- 标签:go 版本管理器,Go,版本管理器
g 是 go 的一个命令行程序,主要用于 go 的本地版本管理。比如,你要同时开发两个 go 项目,一个是 go1.17 版本,另一个是 go1.18 版本,这个时候,比较好的选择就是用 g。
g 的简单使用
在这篇笔记中,我打算逐个了解一下 g 主要命令的使用,然后针对该主要用法的实现去剖析,了解学习实现原理。
g 的安装可以直接参考官方仓库的 readme 文档,其中描述的很详细,这里就不再赘述。
查询可用版本的 go
安装完成后,我们可以通过 g ls-remote stable 查看可用的 go 版本:
$ g ls-remote stable
1.17.9
* 1.18.1
它列出来当前最新的两个稳定版。
实现分析
g 程序的入口很简单只有一行代码:
cli.Run()
Run 函数定义在 cli 包中。通过依赖可以看出,它的实现是基于 github.com/urfave/cli/v2,这是一个专门用于写命令行程序的包。通过它,实例化一个 app,并初始化应用的名称、版本、作者等基础信息以及命令执行前要做的操作:
app := cli.NewApp()
app.Name = "g"
app.Usage = "Golang Version Manager"
// 省略片段 ...
app.Before = func(ctx *cli.Context) (err error) {
ghomeDir = ghome()
goroot = filepath.Join(ghomeDir, "go")
downloadsDir = filepath.Join(ghomeDir, "downloads")
if err = os.MkdirAll(downloadsDir, 0755); err != nil {
return err
}
versionsDir = filepath.Join(ghomeDir, "versions")
return os.MkdirAll(versionsDir, 0755)
}
通常情况下,一个命令行程序可以根据输入的参数而执行不同的操作,比如 git add, git commit 等,这里的 g 程序也是类似,也就是说根据输入参数不同,需要有对应的 handlers,所以就有了 commands:
app.Commands = commands
commands 的定义如下:
commands = []*cli.Command{
{
Name: "ls",
Usage: "List installed versions",
UsageText: "g ls",
Action: list,
},
{
Name: "ls-remote",
Usage: "List remote versions available for install",
UsageText: "g ls-remote [stable|archived|unstable]",
Action: listRemote,
},
// 省略 ...
}
可见,每一个 Command 中定义了命令的名字、用法说明、以及 action(即前面提到的 handler),以 g ls-remote stable 为例,对应的 action 是 listRemote,它的定义位于文件 cli/ls_remote.go 中。
func listRemote(ctx *cli.Context) (err error) {
// ...
c, err := version.NewCollector(url)
if err != nil {
return cli.Exit(errstring(err), 1)
}
var vs []*version.Version
switch channel {
case stableChannel:
vs, err = c.StableVersions()
case unstableChannel:
vs, err = c.UnstableVersions()
case archivedChannel:
vs, err = c.ArchivedVersions()
default:
vs, err = c.AllVersions()
}
// ...
render(inuse(goroot), items, ansi.NewAnsiStdout())
return nil
}
// StableVersions 返回所有稳定版本
func (c *Collector) StableVersions() (items []*Version, err error) {
var divs *goquery.Selection
if c.HasUnstableVersions() {
divs = c.doc.Find("#stable").NextUntil("#unstable")
} else {
divs = c.doc.Find("#stable").NextUntil("#archive")
}
divs.Each(func(i int, div *goquery.Selection) {
vname, ok := div.Attr("id")
if !ok {
return
}
items = append(items, &Version{
Name: strings.TrimPrefix(vname, "go"),
Packages: c.findPackages(div.Find("table").First()),
})
})
return items, nil
}
这段代码中的大致流程如下:
- 识别 stable 参数
- 请求 url(默认为 golang.google.cn/dl/ )查询 go 可下载列表
- 利用 goquery 包匹配 html 页面中的版本名称、对应的链接以及其他相关属性,如图 1.1 所示
- 过滤,取出符合当前命令查询的版本名
- 把结果输出展示
图 1.1
安装指定版本的 go
g 安装一个 go 版本的命令是 g install 1.18.1:
$ g install 1.18.1
Downloading 100% |███████████████| (135/135 MB, 14.563 MB/s)
Computing checksum with SHA256
Checksums matched
Now using go1.18.1
通过输出可以看出,不仅可以实时显示安装进度、下载速度,还展示了计算文件 hash 的校验和及其结果。
实现分析
还记得前面提到的 commands 吗?我们回到这个变量定义的地方:cli/commands.go。其中 install 子命令配置:
{
Name: "install",
Usage: "Download and install a version",
UsageText: "g install <version>",
Action: install,
}
其中的 action 是 install,其实现位于 cli/install.go。函数的大致逻辑有:
- 匹配拿到子命令后的第一个参数,即要安装的版本号
- 获取 url 页面内容,匹配出特定版本的名字和下载链接。如果有多个,则会让用户选择。
- 如果本地不存在,则下载该版本的安装包。
- 解压安装包
- 移除旧的 go 安装目录,并建立新安装的 go 的软链接
可见,关键步骤就是将 go 的安装包解压后,覆盖之前已经存在的 go,如此就完成了目标版本的安装。
切换 go 版本
使用 g 对本地的 go 版本管理最常用的功能可能就是因为平时需要,则多个不同版本之前进行随意切换。g 中切换的命令为:g use 1.18.1:
$ g use 1.18.1
go version go1.18.1 linux/amd64
实现分析
同样的,回到 commands 定义的地方,可以看到 use 的配置:
{
Name: "use",
Usage: "Switch to specified version",
UsageText: "g use <version>",
Action: use,
}
use 子命令后需要追加一个版本号参数。进入参 use 实现代码中看看具体实现:
func use(ctx *cli.Context) (err error) {
vname := ctx.Args().First()
if vname == "" {
return cli.ShowSubcommandHelp(ctx)
}
targetV := filepath.Join(versionsDir, vname)
if finfo, err := os.Stat(targetV); err != nil || !finfo.IsDir() {
return cli.Exit(fmt.Sprintf("[g] The %q version does not exist, please install it first.", vname), 1)
}
_ = os.Remove(goroot)
if err = mkSymlink(targetV, goroot); err != nil {
return cli.Exit(errstring(err), 1)
}
if output, err := exec.Command(filepath.Join(goroot, "bin", "go"), "version").Output(); err == nil {
fmt.Print(string(output))
}
return nil
}
代码大致逻辑如下:
- 从 context 拿到子命令后的版本号参数
- 检查该版本 go 安装包是否存在,如果不存在,则直接提示用户先进行下载。
- 如果已经存在,则移除旧版本(删除软链接)
- 建立新版本的软链接
- 用切换后的 go 执行
go version命令,并输出版本信息
总结
好了,虽然没有将 g 支持的所有命令都一一拿出来分析,但是核心命令的实现都分析完了。至此,你应该心中有了大概的思路 —— 如何实现一个 go 版本管理器。你可以自己尝试实现一个作为练习。如果你也喜欢 g 项目,请去官方仓库下载使用。