本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2021年12月15日 统计字数: 5714字 阅读时间: 12分钟阅读 本文链接: https://soulteary.com/2021/12/15/golang-multi-version-management.html ----- # Golang 多版本管理 如果你是一个 Golang 的用户,那么你大概率会遇到管理和维护 Golang 版本的诉求,如果你恰好同时需要开发调试两个不同版本的项目,在不考虑强制跳版本的情况下,你或许就需要使用“Golang 版本管理工具”来帮助你减轻负担了。 本篇文章将介绍最近几个月,我在使用的工具,它们的优势和不足。希望能够帮助到有类似需求的同学。 ## 写在前面 在本地新旧项目并行开发的过程中,你大概率会遇到一个令人头疼的问题,如何同时使用两个不同版本的 Golang Runtime 进行开发呢? 在容器和 CI 流行的当前时代下,我们似乎已经习惯了用 `docker run` 来切换各种语言的版本,来完成不同项目的开发,基础类型项目的兼容性测试。配合一些支持远程调试的工具,体验似乎也还行。 但是在运行效率和复杂度上,相比本地环境而言,总归是高了那么一丢丢。那么有没有更节能环保的方式呢? ## 基于 Golang 的版本管理工具:voidint/g 最初安装 `gvm` 后,总觉得工具不够“简洁”,所以我基于 [https://github.com/voidint/g/](https://github.com/voidint/g) 调整了一些细节,重新编译了一个版本自用。 如果你不希望自己编译安装,也可以用作者推荐的方式进行安装: ```bash curl -sSL https://raw.githubusercontent.com/voidint/g/master/install.sh | bash ``` 这里如果你是 `oh-my-zsh` 的用户,那么你还需要做一件事,就是解决全局的 `g` 命令的冲突,解决的方式有两种,第一种是在你的 `.zshrc` 文件末尾添加 `unalias` : ```bash echo "unalias g" >> ~/.zshrc # 可选。若其他程序(如'git')使用了'g'作为别名。 # 记得重启 shell ,或者重新 source 配置 ``` 第二种,则是调整 `~/.oh-my-zsh/plugins/git/git.plugin.zsh` 中关于 `g` 的注册,将其注释或删除掉: ```bash # alias g='git' ``` 我的 `.zshrc` 中的完整配置: ```bash # 我的 g 的bin目录调整到了 .gvm ,所以你可能需要一些额外的调整 export PATH="${HOME}/.gvm/bin:$PATH" export GOROOT="${HOME}/.g/go" export PATH="${HOME}/.g/go/bin:$PATH" export G_MIRROR=https://gomirrors.org/ ``` 但是随着使用过程中,我发现在同时使用两个版本的 Golang 的时候,会有一些问题。翻看源码实现,看到了 `https://github.com/voidint/g/blob/master/cli/install.go` 中的安装定义: ```go fmt.Println("Checksums matched") // 删除可能存在的历史垃圾文件 _ = os.RemoveAll(filepath.Join(versionsDir, "go")) // 解压安装包 if err = archiver.Unarchive(filename, versionsDir); err != nil { return cli.NewExitError(errstring(err), 1) } // 目录重命名 if err = os.Rename(filepath.Join(versionsDir, "go"), targetV); err != nil { return cli.NewExitError(errstring(err), 1) } // 重新建立软链接 _ = os.Remove(goroot) if err := mkSymlink(targetV, goroot); err != nil { return cli.NewExitError(errstring(err), 1) } fmt.Printf("Now using go%s\n", v.Name) return nil ``` 发现其实每次版本切换,都将重新建立软链映射。官方项目的 Issue 区,有一个类似的反馈:[\#44](https://github.com/voidint/g/issues/44),作者当时给出了一个 `g` 这个程序之外的解决方案。 所以,如果你的需求比较简单,期望使用一个工具,能够从网上快速的下载 Golang 的预编译版本的 Runtime,并且不需要同时运行多个版本,那么使用 `voidint/g` 就可以满足你的需求了,但是如果你的需求是需要多个版本同时运行,那么你可以接着往下看。 ## 基于 BASH 的版本管理工具:gvm 因为出现了上面的问题,所以我开始考虑调整方案。首先是考虑切换回 [https://github.com/moovweb/gvm](https://github.com/moovweb/gvm),说起 `gvm`,熟悉 Node.js 生态的同学,其实可以很容易联想起 `nvm`。没错,他们的理念是一致的,通过语言生态无关的 Bash 来编写语言管理工具。 在 Node.js 中,因为维护版本下载、更新、删除、切换这些功能和语言无关(比如另外一款工具`n`基于 Node.js),所以其实更健壮一些,不会出现因为 Node.js 配置出现问题, 语言版本管理工具无法运行,出现无法管理语言版本的问题。(鸡生蛋、蛋生鸡的哲学问题)但在 Golang 中,其实预编译的二进制已经和语言无关了,相比之下,使用 Bash 来编写程序,会显得比较“啰嗦”。 这也是我最初没有坚持 `gvm` 的原因之一。除此之外,`gvm` 虽然用户者众,但是很长一段时间作者已经不活跃了,所以在 Issue 和 PR 区都堆积了一堆待办事项。官方的文档中也存在不少错误或者缺失的地方。 **不过,这些都是可解决的**。 `gvm` 之于用户,一般存在三类常见问题: - 程序安装过程中遭遇失败 - 下载 Golang 指定版本失败后无法继续安装 - 用户不知道如何使用镜像资源 先来解决第一个问题,如何正确安装 gvm,官方 ReadMe 中的安装方式在 ZSH 环境中会遇到问题,推荐切换为下面的方式安装: ```bash curl -sSL https://github.com/moovweb/gvm/raw/master/binscripts/gvm-installer | bash ``` 执行过后,我们就可以看到正确的日志输出了: ```bash Cloning from https://github.com/moovweb/gvm.git to /home/ubuntu/.gvm No existing Go versions detected Installed GVM v1.0.22 Please restart your terminal session or to get started right away run `source /home/ubuntu/.gvm/scripts/gvm` ``` 接着我们来看第二个问题,首次安装 Golang 某个版本的时候,因为我们没有配置下载镜像地址,所以可能你的下载会遇到“中断”,获得一个不完全的程序压缩包。程序会判断我们是否已经下载过程序,会尝试优先使用下载过的缓存内容,而不管它是否是完整的,这就导致了一部分用户反复执行 `gvm install go1.17.3 -B` ,但是发现一切正常,就是无法完成版本下载或者切换。 解决这个问题其实也很简单,就是清除掉这个缓存内容: ```bash rm -rf ~/.gvm/archive/go1.17.3.darwin-amd64.tar.gz # or rm -rf ~/.gvm/archive/ ``` 接着我们来看第三个问题,如何使用镜像地址进行下载,加速我们切换 Golang 版本的效率。在官方文档中,有一段使用介绍: ```bash Usage: gvm install [version] [options] -s, --source=SOURCE Install Go from specified source. ... ``` 但是,这个其实并不是我们要的内容,因为它解决的是“指定Golang源代码”的在线地址,而不是预构建的二进制包的地址,在 `https://github.com/moovweb/gvm/blob/master/scripts/install` 中我们可以看到默认使用的是 GitHub 仓库代码,所以如果你希望从零开始源码编译,这个参数可以帮助到你,但是如果你想下载二进制,那么这个参数毫无用处。 ```bash ... GO_SOURCE_URL=https://github.com/golang/go for i in "$@"; do case $i in -s=*|--source=*) GO_SOURCE_URL=$(echo "$i" | sed 's/[-a-zA-Z0-9]*=//') ;; ... ``` 在相同文件的比较靠下的位置,我么可以看到一个名为 `download_binary()` 的函数: ```bash # `GO_BINARY_BASE_URL` env allow user setting base URL for binaries # download, e.g. "https://dl.google.com/go". GO_BINARY_BASE_URL=${GO_BINARY_BASE_URL:-"https://storage.googleapis.com/golang"} GO_BINARY_URL="${GO_BINARY_BASE_URL}/${GO_BINARY_FILE}" GO_BINARY_PATH=${GVM_ROOT}/archive/${GO_BINARY_FILE} if [ ! -f $GO_BINARY_PATH ]; then curl -s -f -L $GO_BINARY_URL > ${GO_BINARY_PATH} if [[ $? -ne 0 ]]; then display_error "Failed to download binary go" rm -rf $GO_INSTALL_ROOT rm -f $GO_BINARY_PATH exit 1 fi fi ``` 这里有一个 `GO_BINARY_BASE_URL` 变量,针对它进行调整,就可以达到我们的目的啦。可惜的是,这个参数自2019年末合并进来之后,并没有更新文档,如果你不阅读代码,基本不会知道还可以从镜像进行资源下载。 这里给出我目前使用的配置,在将下面的配置添加到你的 SHELL 的 `rc` 后,你就可以正常的使用 `gvm` 对 Golang 进行快速的版本切换啦。 ```bash export GO111MODULE=on export GOPROXY=https://goproxy.io,direct # or # exort GOPROXY="https://goproxy.cn" export GOPATH="$HOME/go" PATH="$GOPATH/bin:$PATH" export GO_BINARY_BASE_URL=https://golang.google.cn/dl/ [[ -s "$HOME/.gvm/scripts/gvm" ]] && source "$HOME/.gvm/scripts/gvm" export GOROOT_BOOTSTRAP=$GOROOT ``` 至于切换不同版本 Golang ,也很简单,只需要两条条命令: ```bash gvm install go1.17.3 -B gvm use go1.17.3 ``` 倘若你期望不借助 Golang 团队官方镜像,完全定制一个 Golang Base 的 Docker 的镜像,相比较其他工具,`gvm` 会是一个简单的选择,不需要预构建、也不挑系统。 ## 来自官方的解决方案:golang/dl 如果你不喜欢来自三方的解决方案,那么或许可以试试来自官方的方案。(前提是,你不需要同时运行多个版本的 Golang) 相比较社区方案,官方的方案就更有趣了:[https://github.com/golang/dl](https://github.com/golang/dl)。官方维护了自 1.5 以来到 1.17 的所有版本的更新软件包。 我们可以通过安装普通软件包的方式来获取具体版本的安装工具,以及进行“覆盖安装”: ```bash go get golang.org/dl/go1.17.3 go1.17.3 download ``` 不过和上面不同的是,[https://github.com/golang/dl/blob/master/internal/version/version.go](https://github.com/golang/dl/blob/master/internal/version/version.go)中的写死的逻辑会让你安装的目录在用户目录的 `sdk` 文件夹中,所以如果你使用这种方式,`export` 的路径需要做一个调整: ```go func goroot(version string) (string, error) { home, err := homedir() if err != nil { return "", fmt.Errorf("failed to get home directory: %v", err) } return filepath.Join(home, "sdk", version), nil } ``` ## 其他 此外,还有两个有趣的项目,借鉴自 Rustup 的 :[https://github.com/owenthereal/goup](https://github.com/owenthereal/goup);以及借鉴 rbenv和pyenv的:[https://github.com/syndbg/goenv](https://github.com/syndbg/goenv)。 ## 最后 最近在持续做笔记内容整理的事情,恰好看到这篇笔记草稿,顺手整理成文。 本篇就先写到这里啦,希望能够帮你节约一些时间,避过小坑。 --EOF