Golang 多版本管理

2021-12-29 折騰技術

如果你是一個 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。

最後

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

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

--EOF


如果你覺得內容還算實用,歡迎點讚分享給你的朋友,在此謝過。


本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或重新修改使用,但需要註明來源。署名 4.0 國際 (CC BY 4.0)

本文作者: 蘇洋

創建時間: 2021年12月15日統計字數: 5707字閱讀時間: 12分鐘閱讀本文連結:

相關焦點

  • golang之context使用
    背景golang中並發編程的三種實現方式:chan管道、waitGroup和Context。本篇將重點介紹context的使用,告訴大家基本的使用方式,做到會用。Context概念介紹context譯為上下文,golang在1.6.2的時候還沒有自己的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的庫中。golang 的 Context包,是專門用來處理多個goroutine之間與請求域的數據、取消信號、截止時間等相關操作。
  • 如何使用容器build多平臺golang程序
    問題 我們採用vendor來做golang項目的依賴管理, build的docker鏡像拉取的官方的golang官方鏡像, 按照鏡像的使用說明:$ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:1.6 go build -v但是卻沒有成功, 我項目下面的vendor的依賴並沒有被go找到。
  • 使用Golang快速構建WEB應用
    如果發現問題或者有好的建議請回復我我回及時更正。 1.Abstract在學習web開發的過程中會遇到很多困難,因此寫了一篇類似綜述類的文章。作為路線圖從web開發要素的index出發來介紹golang開發的學習流程以及Example代碼。在描述中多是使用代碼來描述使用方法不會做過多的說明。最後可以方便的copy代碼來實現自己的需求。
  • Go Modules v2 及後續版本
    原文地址:https://blog.golang.org/v2-go-modules簡介這個系列的文章總共有五篇,這是第四篇:隨著一個項目的成熟和新需求的增加,過去的特性和設計決策可能要被廢棄。開發人員可能希望通過刪除棄用的函數、重命名類型或將複雜的包拆分為可管理的部分來實踐所學。
  • Golang入門教程——map篇
    今天是golang專題的第7篇文章,我們來聊聊golang當中map的用法。map這個數據結構我們經常使用,存儲的是key-value的鍵值對。在C++/java當中叫做map,在Python中叫做dict。
  • Golang指南:頂級Golang框架、IDE和工具列表
    (點擊尾部閱讀原文前往)原文:https://dzone.com/articles/golang-guide-a-list-of-top-golang-frameworks-ides自推出以來,Google的Go程式語言(Golang)越來越受主流用戶的歡迎。
  • golang中的fallthrough
    收錄於話題 #golang因為fallthrough不能孤立使用,需要在switch語句中使用,使用方法和break接近。不加break和fallthrough先看代碼。
  • Golang入門教程——面向對象篇
    golang作為一門剛剛誕生十年的新興語言自然是支持面向對象的,但是golang當中面向對象的概念和特性與我們之前熟悉的大部分語言都不盡相同。比如Java、Python等,相比之下, golang這個部分的設計非常得簡潔和優雅(仁者見仁),所以即使你之前沒有系統地了解過面向對象,也沒有關係,也一定能夠看懂。
  • Golang入門教程——基本操作篇
    func add(x, y int) int {    return x + y}多值返回在前面介紹golang特性的時候曾經提到過,golang作為一個看起來很守舊的語言,但是卻支持很多新鮮的特性。func sample() (string, string) {    return "sample1", "sample2"}多值返回也會有一個小小的問題,就是如果我們要返回的值過多,會導致這個return會寫得很長,或者是組裝的邏輯變得很複雜。
  • Golang的字符編碼與regexp
    需要注意的是,生成前綴字符串時其底層將調用 strings.Builder 的 WriteRune() 函數(https://github.com/golang/go/blob/master/src/regexp/syntax/prog.go#L147),內部將調用 utf8.EncodeRune() 強制轉換表達式的字符為 UTF-8 編碼(如:\xff => \xc3\xbf)。
  • Golang:重新認識你的Go應用
    在大多數情況下,我們都會使用Go的默認設置,也就是線程數等於CPU格式,在這種情況下不會觸發作業系統的線程調度和上下文切換,所有的調度都會發生在用戶態,由Go語言調度器觸發,能夠減少非常多的額外開銷。4 內存管理:Heap vs Stack4.1 堆棧之傻傻分不清楚
  • Golang最細節篇— struct{} 空結構體究竟是啥?
    ,golang 內存管理分析。空結構體:var s struct{}// 變量 size 是 0 ;fmt.Println(unsafe.Sizeof(s))該空結構體的變量佔用內存 0 字節。換句話說,在 golang 裡面,涉及到所有內存 size 為 0 的內存分配,那麼就是用的同一個地址 &zerobase 。
  • Golang 與系統調用
    【導讀】本文介紹了使用 golang 和系統調用實現一個strace的操作。GopherCon2017 中的一個視頻講解了如何用golang實現一個簡單的strace,本文是基於此演講整理而來。myStrace了解以上內容後,presenter 現場實現了一個go版本的strace, 需要在 linux amd64 環境編譯。
  • gRPC 實操指南(golang)
    •C/S架構的傳輸業務,如股票軟體,每天需要用戶登陸的時候去伺服器拉取最新的數據,或者較簡單的文件傳輸業務,登陸驗證業務,證書業務都可以使用rpc的方式•跨語言開發的項目,比如web業務使用golang進行開發,底層使用cpp或c,部分腳本使用py,跨語言通信可以通過RPC提供的不同語言的開發機制進行實現。
  • golang 打包到docker運行最小鏡像
    1.在 https://hub.docker.com/ 中 搜索 golang ,Dockerfile 中 依賴 golang 鏡像 (大概100M左右)這種方式 如下,是在容器裡,將 golang 程序編譯的。
  • golang下文件鎖的使用
    前言題目是golang下文件鎖的使用,但本文的目的其實是通過golang下的文件鎖的使用方法,來一窺文件鎖背後的機制。
  • 【Golang】圖解channel之數據結構
    channel被設計用來實現goroutine間的通信,按照golang的設計思想:以通信的方式共享內存。
  • golang channel 使用總結
    本文介紹了使用 golang channel 的諸多特性和技巧,已經熟悉了 go 語言特性的小夥伴也可以看看,很有啟發。不同於傳統的多線程並發模型使用共享內存來實現線程間通信的方式,golang 的哲學是通過 channel 進行協程 (goroutine) 之間的通信來實現數據共享:Do not communicate by sharing memory; instead, share memory by communicating.
  • 聊聊golang的defer
    序本文主要研究一下golang的deferdeferreturn先賦值(對於命名返回值),然後執行defer,最後函數返回
  • golang的空結構體struct{}介紹
    簡介空結構體:不含任何欄位的struct類型我們稱之為空結構體,即struct{}由於空結構體不包含欄位,也就是沒有任何數據,因此所佔內存大小空間為0var s struct{}fmt.Println(unsafe.Sizeof