Golang 多版本管理

如果你是一個 Golang 的用戶,那麼你大概率會遇到管理和維護 Golang 版本的訴求,如果你恰好同時需要開發調試兩個不同版本的項目,在不考慮強制跳版本的情況下,你或許就需要使用 “Golang 版本管理工具” 來幫助你減輕負擔了。

本篇文章將介紹最近幾個月,我在使用的工具,它們的優勢和不足。希望能夠幫助到有類似需求的同學。

寫在前面

在本地新舊項目並行開發的過程中,你大概率會遇到一個令人頭疼的問題,如何同時使用兩個不同版本的 Golang Runtime 進行開發呢?

在容器和 CI 流行的當前時代下,我們似乎已經習慣了用 docker run 來切換各種語言的版本,來完成不同項目的開發,基礎類型項目的兼容性測試。配合一些支持遠程調試的工具,體驗似乎也還行。

但是在運行效率和複雜度上,相比本地環境而言,總歸是高了那麼一丟丟。那麼有沒有更節能環保的方式呢?

基於 Golang 的版本管理工具:voidint/g

最初安裝 gvm 後,總覺得工具不夠 “簡潔”,所以我基於 https://github.com/voidint/g/ 調整了一些細節,重新編譯了一個版本自用。

如果你不希望自己編譯安裝,也可以用作者推薦的方式進行安裝:

curl -sSL https://raw.githubusercontent.com/voidint/g/master/install.sh | bash

這裏如果你是 oh-my-zsh 的用戶,那麼你還需要做一件事,就是解決全局的 g  命令的衝突,解決的方式有兩種,第一種是在你的 .zshrc 文件末尾添加 unalias

echo "unalias g" >> ~/.zshrc # 可選。若其他程序(如'git')使用了'g'作爲別名。
# 記得重啓 shell ,或者重新 source 配置

第二種,則是調整 ~/.oh-my-zsh/plugins/git/git.plugin.zsh 中關於 g 的註冊,將其註釋或刪除掉:

# alias g='git'

我的 .zshrc 中的完整配置:

# 我的 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 中的安裝定義:

 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,作者當時給出了一個 g 這個程序之外的解決方案。

所以,如果你的需求比較簡單,期望使用一個工具,能夠從網上快速的下載 Golang 的預編譯版本的 Runtime,並且不需要同時運行多個版本,那麼使用 voidint/g 就可以滿足你的需求了,但是如果你的需求是需要多個版本同時運行,那麼你可以接着往下看。

基於 BASH 的版本管理工具: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 之於用戶,一般存在三類常見問題:

先來解決第一個問題,如何正確安裝 gvm,官方 ReadMe 中的安裝方式在 ZSH 環境中會遇到問題,推薦切換爲下面的方式安裝:

curl -sSL https://github.com/moovweb/gvm/raw/master/binscripts/gvm-installer | 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 ,但是發現一切正常,就是無法完成版本下載或者切換。

解決這個問題其實也很簡單,就是清除掉這個緩存內容:

rm -rf ~/.gvm/archive/go1.17.3.darwin-amd64.tar.gz
# or
rm -rf ~/.gvm/archive/

接着我們來看第三個問題,如何使用鏡像地址進行下載,加速我們切換 Golang 版本的效率。在官方文檔中,有一段使用介紹:

Usage: gvm install [version] [options]
    -s,  --source=SOURCE      Install Go from specified source.
...

但是,這個其實並不是我們要的內容,因爲它解決的是 “指定 Golang 源代碼” 的在線地址,而不是預構建的二進制包的地址,在 https://github.com/moovweb/gvm/blob/master/scripts/install 中我們可以看到默認使用的是 GitHub 倉庫代碼,所以如果你希望從零開始源碼編譯,這個參數可以幫助到你,但是如果你想下載二進制,那麼這個參數毫無用處。

...
 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() 的函數:

 # `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 進行快速的版本切換啦。

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 ,也很簡單,只需要兩條條命令:

gvm install go1.17.3 -B
gvm use go1.17.3

倘若你期望不借助 Golang 團隊官方鏡像,完全定製一個 Golang Base 的 Docker 的鏡像,相比較其他工具,gvm 會是一個簡單的選擇,不需要預構建、也不挑系統。

來自官方的解決方案:golang/dl

如果你不喜歡來自三方的解決方案,那麼或許可以試試來自官方的方案。(前提是,你不需要同時運行多個版本的 Golang)

相比較社區方案,官方的方案就更有趣了:https://github.com/golang/dl。官方維護了自 1.5 以來到 1.17 的所有版本的更新軟件包。

我們可以通過安裝普通軟件包的方式來獲取具體版本的安裝工具,以及進行 “覆蓋安裝”:

go get golang.org/dl/go1.17.3
go1.17.3 download

不過和上面不同的是,https://github.com/golang/dl/blob/master/internal/version/version.go 中的寫死的邏輯會讓你安裝的目錄在用戶目錄的 sdk 文件夾中,所以如果你使用這種方式,export 的路徑需要做一個調整:

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;以及借鑑 rbenv 和 pyenv 的:https://github.com/syndbg/goenv。

最後

最近在持續做筆記內容整理的事情,恰好看到這篇筆記草稿,順手整理成文。

本篇就先寫到這裏啦,希望能夠幫你節約一些時間,避過小坑。

本文作者: 蘇洋

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/v_HCQacOFzbuHgNbyVi4oQ