我這樣升級 Go 版本,你呢?
閱讀本文大概需要 5 分鐘。
大家好,我是 polarisxu。
有些人可能注意到,每次 Go 發(fā)布新版本,官方都會提供類似這樣的升級截圖:

這可以說是官方的 Go 多版本管理,也是升級 Go 的方式。今天就一起聊一聊這種多版本管理方式及其實(shí)現(xiàn)原理。(我之前介紹過一個(gè)第三方多版本管理工具 goup,是我比較推薦的)。
注意,Windows 用戶應(yīng)該使用 WSL2。
01 為什么需要多個(gè) Go 版本
有些人可能覺得沒有這樣的需求。實(shí)際工作中,這樣的需求還是很常見的。以下一些場景,可能會希望有多版本:
一般為了穩(wěn)定,線上版本通常不會激進(jìn)升級到最新版本,但你本地很可能想試用新版本的功能。這時(shí)候就希望能方便的支持多版本; 為了測試或重現(xiàn)特定的問題,希望能夠在特定的版本進(jìn)行,這是為了避免不同版本干擾。 。。。
多版本并存,讓我們可以更自如的使用 Go。
02 官方多版本的使用方式
根據(jù)上面的圖,安裝某個(gè)版本的 Go,跟一般 Go 包安裝一樣,執(zhí)行 go get 命令:
$ go get golang.org/dl/go<version> // 其中 <version> 替換為你希望安裝的 Go 版本
這一步,只是安裝了一個(gè)特定 Go 版本的包裝器,真正安裝特定的 Go 版本,還需要執(zhí)行如下命令:
$ go<version> download // 和上面一樣,<version> 是具體的版本
因此,如果需要安裝 Go1.16.4,執(zhí)行如下兩個(gè)命令即可。
$ go get golang.org/dl/go1.16.4
$ go1.16.4 download
幾個(gè)注意的點(diǎn):
有一個(gè)特殊的版本標(biāo)記:gotip,用來安裝最新的開發(fā)版本; 因?yàn)?golang.org 訪問不了,你應(yīng)該配置 GOPROXY(所以,啟用 Module 是必須的); 跟安裝其他包一樣,go get 之后,go1.16.4 這個(gè)命令會被安裝到 $GOBIN目錄下,默認(rèn)是~/go/bin目錄,所以該目錄應(yīng)該放入 PATH 環(huán)境變量;沒有執(zhí)行 download 之前,運(yùn)行 go1.16.4,會提示 go1.16.4: not downloaded. Run 'go1.16.4 download' to install to ~/sdk/go1.16.4;
可見,最后下載下來的 Go 放在了 ~/sdk/go1.16.4 目錄下。
現(xiàn)在你是否有這樣的疑問:沒執(zhí)行 download 之前,直接運(yùn)行 go1.16.4 會報(bào)錯(cuò),執(zhí)行之后,它就成了具體的 Go 命令了,怎么做到的?
03 扒一扒原理
golang.org/dl/go<version> 對應(yīng)的源碼在 https://github.com/golang/dl(這是一個(gè)鏡像)。
查看該倉庫代碼,發(fā)現(xiàn)一堆以各個(gè)版本命名的目錄:

可見,每次發(fā)布新版本,都需要往這個(gè)倉庫增加一個(gè)對應(yīng)的版本文件夾。
隨便打開一個(gè)(比如 go1.16.4),看看里面包含什么文件:
就一個(gè) main.go 文件(從 go get 安裝操作,你應(yīng)該猜到一定有一個(gè) main.go 文件)。
main.go 文件的內(nèi)容如下:(gotip 的內(nèi)容不一樣,它調(diào)用的是 version.RunTip())
package main
import "golang.org/dl/internal/version"
func main() {
version.Run("go1.16.4")
}
所以,關(guān)鍵在于 internal/version 包的 Run 函數(shù)(不同版本,version 參數(shù)不同)。注意以下代碼我給的注釋:
// Run runs the "go" tool of the provided Go version.
func Run(version string) {
log.SetFlags(0)
// goroot 獲取 go 安裝的目錄,即 ~/sdk/go<version>
root, err := goroot(version)
if err != nil {
log.Fatalf("%s: %v", version, err)
}
// 執(zhí)行下載操作
if len(os.Args) == 2 && os.Args[1] == "download" {
if err := install(root, version); err != nil {
log.Fatalf("%s: download failed: %v", version, err)
}
os.Exit(0)
}
// 怎么驗(yàn)證是否已經(jīng)下載好了 Go?在下載的 Go 中會創(chuàng)建一個(gè) .unpacked-success 文件,用來指示下載好了。
if _, err := os.Stat(filepath.Join(root, unpackedOkay)); err != nil {
log.Fatalf("%s: not downloaded. Run '%s download' to install to %v", version, version, root)
}
// 運(yùn)行下載好的 Go
runGo(root)
}
這里主要是下載和運(yùn)行 Go。
下載
我們先看下載、安裝 Go。
當(dāng)執(zhí)行 go1.16.4 download 時(shí),會運(yùn)行 install 函數(shù),查看該函數(shù)發(fā)現(xiàn),它調(diào)用了 versionArchiveURL 函數(shù)獲取要下載的 Go 的 URL:
// versionArchiveURL returns the zip or tar.gz URL of the given Go version.
func versionArchiveURL(version string) string {
goos := getOS()
ext := ".tar.gz"
if goos == "windows" {
ext = ".zip"
}
arch := runtime.GOARCH
if goos == "linux" && runtime.GOARCH == "arm" {
arch = "armv6l"
}
return "https://dl.google.com/go/" + version + "." + goos + "-" + arch + ext
}
也就是從 https://dl.google.com 下載 Go 包,最終的包(是一個(gè)歸檔文件,Wiindows 下是 .zip,其他系統(tǒng)是 .tar.gz)會放到 ~/sdk/go1.16.4 目錄下。
之后通過 sha256 驗(yàn)證文件的完整性(因?yàn)榉?wù)端放了 sha256 校驗(yàn)文件),最后解壓縮,并創(chuàng)建上面說的 .unpacked-success 空標(biāo)記文件。這樣這個(gè)版本的 Go 就安裝成功了。
注意,gotip 的下載是通過 git 獲取源碼的方式進(jìn)行的,它會通過源碼構(gòu)建安裝最新的 gotip 版本。具體邏輯在 internal/version/gotip.go 中。
運(yùn)行
因?yàn)橄螺d的 Go 是預(yù)編譯好的,因此可以直接使用。
但是它將 Go 下載到了 ~/sdk/go<version> 目錄下了,我們并沒有將這個(gè)目錄的 bin 目錄加入 PATH,因此直接 go 命令運(yùn)行的還是之前的版本,而不是剛安裝的 go1.16.4。這個(gè)問題我們一會再說,先看看為什么這個(gè)時(shí)候 go1.16.4 命令可以當(dāng)作 go 命令來使用。
上文說了,go1.16.4 只是一個(gè)包裝器。當(dāng)對應(yīng)的 Go1.16.4 安裝成功后,再次運(yùn)行 go1.16.4,會執(zhí)行 internal/version/version.go 中的 runGo(root) 函數(shù)。
func runGo(root string) {
gobin := filepath.Join(root, "bin", "go"+exe())
cmd := exec.Command(gobin, os.Args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
newPath := filepath.Join(root, "bin")
if p := os.Getenv("PATH"); p != "" {
newPath += string(filepath.ListSeparator) + p
}
cmd.Env = dedupEnv(caseInsensitiveEnv, append(os.Environ(), "GOROOT="+root, "PATH="+newPath))
handleSignals()
if err := cmd.Run(); err != nil {
// TODO: return the same exit status maybe.
os.Exit(1)
}
os.Exit(0)
}
該函數(shù)通過 os/exec 包運(yùn)行 ~/sdk/go1.16.4/bin/go 命令,并設(shè)置好響應(yīng)的標(biāo)準(zhǔn)輸入輸出流等,同時(shí)為新運(yùn)行的進(jìn)程設(shè)置好相關(guān)環(huán)境變量,可以認(rèn)為,執(zhí)行 go1.16.4,相當(dāng)于執(zhí)行 ~/sdk/go1.16.4/bin/go。
所以,go1.16.4 這個(gè)命令,一直都只是一個(gè)包裝器。如果你希望新安裝的 go1.16.4 成為系統(tǒng)默認(rèn)的 Go 版本,即希望運(yùn)行 go 運(yùn)行的是 go1.16.4,方法有很多:
將 ~/sdk/go1.16.4/bin/go加入 PATH 環(huán)境變量(替換原來的);做一個(gè)軟連,默認(rèn) go 執(zhí)行 go1.16.4(推薦這種方式),不需要頻繁修改 PATH; 移動 go1.16.4 替換之前的 go(不推薦);
03 每次升級版本創(chuàng)建一個(gè)包裝器
手動復(fù)制粘貼代碼做這件事情肯定是很笨的辦法。在 golang.org/dl 中提供了一個(gè)工具,可以快速生成對應(yīng)版本的包裝器:https://github.com/golang/dl/blob/master/internal/genv/main.go。
$ genv go1.16.4
就可以生成 go1.16.4 包裝器。這里的實(shí)現(xiàn),有一個(gè)點(diǎn)提一下,它使用了 go list -m -json 命令:
$ go list -m -json
{
"Path": "golang.org/dl",
"Main": true,
"Dir": "<workspace>/dl",
"GoMod": "<workspace>/dl/go.mod",
"GoVersion": "1.11"
}
可以方便解析相關(guān)信息。
04 總結(jié)
官方的 Go 多版本管理就介紹完了??偨Y(jié)一下:
官方通過 genv 命令生成對應(yīng)版本的包裝器; 通過 go get 命令下載安裝對應(yīng)的包裝器; 運(yùn)行包裝器,提供 download 這個(gè) flag,下載對應(yīng)版本的 Go 安裝包并解壓、校驗(yàn); 之后,運(yùn)行包裝器,會執(zhí)行對應(yīng)版本的 go 命令;
這樣達(dá)到了多版本管理的目的。這個(gè)設(shè)計(jì)思路還是可以的。
但這種多版本管理,我認(rèn)為存在一些問題:
上面說的,讓某個(gè)版本成為默認(rèn) Go 版本,沒有命令一鍵搞定; 沒法知道有哪些版本,比如無法方便的知曉 1.15.13 是否存在,更無法方便的知曉 1.15.x 系列,x 的最大版本; 刪除某個(gè)版本,得手動進(jìn)行(刪除包裝器和下載的 Go 安裝包);
你喜歡這種方式管理還是類似 goup 這樣的第三方工具呢?你現(xiàn)在是怎么管理多版本的,歡迎交流!
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標(biāo)準(zhǔn)庫》等。
堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
