升級 Go 版本


有些人可能注意到,每次 Go 發布新版本,官方都會提供類似這樣的升級截圖:

 

 

 

這可以說是官方的 Go 多版本管理,也是升級 Go 的方式。今天就一起聊一聊這種多版本管理方式及其實現原理。(我之前介紹過一個第三方多版本管理工具 goup,是我比較推薦的)。

注意,Windows 用戶應該使用 WSL2。

01 為什么需要多個 Go 版本

有些人可能覺得沒有這樣的需求。實際工作中,這樣的需求還是很常見的。以下一些場景,可能會希望有多版本:

  • 一般為了穩定,線上版本通常不會激進升級到最新版本,但你本地很可能想試用新版本的功能。這時候就希望能方便的支持多版本;
  • 為了測試或重現特定的問題,希望能夠在特定的版本進行,這是為了避免不同版本干擾。
  • 。。。

多版本並存,讓我們可以更自如的使用 Go。

02 官方多版本的使用方式

根據上面的圖,安裝某個版本的 Go,跟一般 Go 包安裝一樣,執行 go get 命令:

$ go get golang.org/dl/go<version>  // 其中 <version> 替換為你希望安裝的 Go 版本

  

這一步,只是安裝了一個特定 Go 版本的包裝器,真正安裝特定的 Go 版本,還需要執行如下命令:

$ go<version> download   // 和上面一樣,<version> 是具體的版本

  

因此,如果需要安裝 Go1.16.4,執行如下兩個命令即可。

$ go get golang.org/dl/go1.16.4
$ go1.16.4 download

  

幾個注意的點:

  • 有一個特殊的版本標記:gotip,用來安裝最新的開發版本;
  • 因為 golang.org 訪問不了,你應該配置 GOPROXY(所以,啟用 Module 是必須的);
  • 跟安裝其他包一樣,go get 之后,go1.16.4 這個命令會被安裝到 $GOBIN 目錄下,默認是 目錄,所以該目錄應該放入 PATH 環境變量;
  • 沒有執行 download 之前,運行 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 目錄下。

現在你是否有這樣的疑問:沒執行 download 之前,直接運行 go1.16.4 會報錯,執行之后,它就成了具體的 Go 命令了,怎么做到的?

03 扒一扒原理

golang.org/dl/go<version> 對應的源碼在 https://github.com/golang/dl(這是一個鏡像)。

查看該倉庫代碼,發現一堆以各個版本命名的目錄:

 

 

 

可見,每次發布新版本,都需要往這個倉庫增加一個對應的版本文件夾。

隨便打開一個(比如 go1.16.4),看看里面包含什么文件:

就一個 main.go 文件(從 go get 安裝操作,你應該猜到一定有一個 main.go 文件)。

main.go 文件的內容如下:(gotip 的內容不一樣,它調用的是 version.RunTip())

package main

import "golang.org/dl/internal/version"

func main() {
 version.Run("go1.16.4")
}

  

所以,關鍵在於 internal/version 包的 Run 函數(不同版本,version 參數不同)。注意以下代碼我給的注釋:

// 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)
 }

  // 執行下載操作
 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)
 }

  // 怎么驗證是否已經下載好了 Go?在下載的 Go 中會創建一個 .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)
 }

  // 運行下載好的 Go
 runGo(root)
}

  

這里主要是下載和運行 Go。

下載

我們先看下載、安裝 Go。

當執行 go1.16.4 download 時,會運行 install 函數,查看該函數發現,它調用了 versionArchiveURL 函數獲取要下載的 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 包,最終的包(是一個歸檔文件,Wiindows 下是 .zip,其他系統是 .tar.gz)會放到 ~/sdk/go1.16.4 目錄下。

之后通過 sha256 驗證文件的完整性(因為服務端放了 sha256 校驗文件),最后解壓縮,並創建上面說的 .unpacked-success 空標記文件。這樣這個版本的 Go 就安裝成功了。

注意,gotip 的下載是通過 git 獲取源碼的方式進行的,它會通過源碼構建安裝最新的 gotip 版本。具體邏輯在 internal/version/gotip.go 中。

運行

因為下載的 Go 是預編譯好的,因此可以直接使用。

但是它將 Go 下載到了 ~/sdk/go<version> 目錄下了,我們並沒有將這個目錄的 bin 目錄加入 PATH,因此直接 go 命令運行的還是之前的版本,而不是剛安裝的 go1.16.4。這個問題我們一會再說,先看看為什么這個時候 go1.16.4 命令可以當作 go 命令來使用。

上文說了,go1.16.4 只是一個包裝器。當對應的 Go1.16.4 安裝成功后,再次運行 go1.16.4,會執行 internal/version/version.go 中的 runGo(root) 函數。

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)
}

  

該函數通過 os/exec 包運行 ~/sdk/go1.16.4/bin/go 命令,並設置好響應的標准輸入輸出流等,同時為新運行的進程設置好相關環境變量,可以認為,執行 go1.16.4,相當於執行 ~/sdk/go1.16.4/bin/go

所以,go1.16.4 這個命令,一直都只是一個包裝器。如果你希望新安裝的 go1.16.4 成為系統默認的 Go 版本,即希望運行 go 運行的是 go1.16.4,方法有很多:

  • 將 ~/sdk/go1.16.4/bin/go 加入 PATH 環境變量(替換原來的);
  • 做一個軟連,默認 go 執行 go1.16.4(推薦這種方式),不需要頻繁修改 PATH;
  • 移動 go1.16.4 替換之前的 go(不推薦);

03 每次升級版本創建一個包裝器

手動復制粘貼代碼做這件事情肯定是很笨的辦法。在 golang.org/dl 中提供了一個工具,可以快速生成對應版本的包裝器:https://github.com/golang/dl/blob/master/internal/genv/main.go。

$ genv go1.16.4

  

就可以生成 go1.16.4 包裝器。這里的實現,有一個點提一下,它使用了 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"
}

  

04 總結

官方的 Go 多版本管理就介紹完了。總結一下:

  • 官方通過 genv 命令生成對應版本的包裝器;
  • 通過 go get 命令下載安裝對應的包裝器;
  • 運行包裝器,提供 download 這個 flag,下載對應版本的 Go 安裝包並解壓、校驗;
  • 之后,運行包裝器,會執行對應版本的 go 命令;

這樣達到了多版本管理的目的。這個設計思路還是可以的。

但這種多版本管理,我認為存在一些問題:

  • 上面說的,讓某個版本成為默認 Go 版本,沒有命令一鍵搞定;
  • 沒法知道有哪些版本,比如無法方便的知曉 1.15.13 是否存在,更無法方便的知曉 1.15.x 系列,x 的最大版本;
  • 刪除某個版本,得手動進行(刪除包裝器和下載的 Go 安裝包);

 

 

from the 我這樣升級 Go 版本

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM