1. 程式人生 > 程式設計 >Go 模組存在的意義與解決的問題

Go 模組存在的意義與解決的問題

本文首發於我的部落格,如果有用,歡迎點贊收藏,讓更多的朋友看到。

作者:William Kennedy | 原文:Modules Part 01: Why And What

最近,我在嘗試整理一篇關於 Go 包管理髮展歷史的文章,希望能加深自己對這一塊知識的認識。在蒐集資料的時候,發現了這篇文章,順手翻譯了一下。

本文是該系列的第一篇,主要介紹包依賴管理中一些基礎知識。文中提出了 Go 開發中的三個痛點,如何解決只能在 GOPATH 指定路徑開發,如何實現有效的版本管理,以及如何支援 Go 原生工具集依賴管理。針對它們,Go Module 都提供了相應的解決方案。

從第一篇的內容上看,作者後面的文章應該會對 Go 的模組機制進行詳細的剖析,很期待。話說,總感覺這篇文章翻譯的有點彆扭,檢查的時候發現有好幾處語義理解錯誤,尷尬。

翻譯正文如下:


介紹

Go Module 是 Go 為包依賴管理提供的一個綜合性解決方案。從 Go 初版釋出以來,Go 開發者針對包管理這一塊提出過三個痛點問題。

如何實現在 GOPATH 工作區之外進行程式碼開發;

如何實現依賴版本化管理和有效識別出使用依賴的相容性問題;

如何實現通過 Go 原生工具進行依賴管理;

隨著 Go 1.13 的釋出,這三個問題都得到了解決。在過去的兩年裡,Go 團隊成員為此付出了巨大的努力。本文中將重點介紹從 GOPATH 到模組機制的變化,還有模組究竟解決了什麼問題。我將通過足夠易懂的語言向大家說明模組的工作機制。

我覺得,重點要理解為什麼模組這樣工作。

GOPATH

GOPATH 是用於指定 Go 工作區的物理位置,一直以來都很好地服務著 Go 的開發者們。但它對非 Go 開發者並不友好,想在沒有任何配置的情況下,隨時隨地進行 Go 開發,這是不可能的一件事。

Go 團隊要解決的第一個問題就是允許 Go 的原始碼倉庫能被 clone 在磁碟中的任意位置,而不僅僅是 GOPATH 指定的工作區。並且 Go 工具集仍然要能成功定位、編譯構建與測試它們。

上圖展示了一個 github 倉庫,ardanlabs/conf,這個倉庫僅有一個包,它用於提供對應用配置處理的支援。

以前,如果想使用這個包,我們需要通過 go get 並指定倉庫的規範化名稱實現下載一份到你的 GOPATH 下。倉庫規範化的名稱是由遠端倉庫的基礎 url 和倉庫名稱兩部分組成。

一個例子,在 Go Module 之前,如果你執行 go get github.com/ardanlabs/conf,程式碼將會被 clone 到 $GOPATH/src/github.com/ardanlabs/conf 目錄下。基於 GOPATH 和倉庫名,無論我們把工作區設定何處,Go 工具集始終都能正確地找到程式碼的位置。

匯入解析

清單 1

github.com/ardanlabs/c…

package conf_test

import (
    ...
    "github.com/ardanlabs/conf"
    ...
)
複製程式碼

清單1 顯示了 conf 包中測試檔案 conf_test.go 中的匯入其他包的程式碼片段。

當測試包名用 _test 命名,這就意味著測試程式碼和被測試程式碼是在不同的包中,測試程式碼必須匯入要被測試的外部程式碼。從上面的程式碼片段中,我們可以看出,測試程式碼是如何將 conf 匯入的。基於 GOPATH 機制,可以非常容易地解析出匯入包的路徑。然後,Go 工具集就可以成功定位、編譯和測試程式碼。

如果 GOPATH 不存在或者目錄結構與倉庫名稱不匹配,將會如何呢?

清單 2

import "github.com/ardanlabs/conf"

// GOPATH mode: Physical location on disk matches the GOPATH
// and Canonical name of the repo.
// GOPATH 模式:磁碟物理位置與 GOPATH 和倉庫的規範名稱相匹配
$GOPATH/src/github.com/ardanlabs/conf

// Module mode: Physical location on disk doesn’t represent
// the Canonical name of the repo.
// Module 模式:磁碟上的物理位置和倉庫全名沒有必然的匹配關係。
/users/bill/conf
複製程式碼

清單2 展示瞭如果把倉庫 clone 到任意位置將會產生什麼問題。當開發者選擇將程式碼下載他們希望的任意位置時,通過 import 包名稱解析出原始碼的實際位置就不行了。

如何解決這個問題?

我們可以指定一個特殊的檔案,使用它指定倉庫的規範名稱。這個檔案的位置可理解為是 GOPATH 的一個替代,在它其中定義了倉庫的規範名稱,Go 工具可以通過這個名稱解析原始碼中匯入包的位置,而不必關心倉庫被 clone 到了什麼地方。

我們把這個特殊的檔案命名為 go.mod,將在這個檔案中定義的由規範名稱表示的新實體稱為 Module。

清單 3

github.com/ardanlabs/c…

module github.com/ardanlabs/conf
複製程式碼

清單3 中顯示了 conf 倉庫中的 go.mod 檔案的第一行 。

這一行定義了模組的名稱,它同時也代表了倉庫全名,開發者期待使用它來引用庫中任意部分的程式碼。現在,庫被下載到什麼位置已經不再那麼重要了,Go 工具集會根據 module 檔案所在位置和模組名定位和解析內部包的匯入,比如前面的示例中,在測試檔案中的匯入 conf 包。

現在,模組機制允許我們將程式碼下載到任意位置。那下一個要解決的問題就是如何將程式碼捆綁到一起進行版本控制。

捆綁和版本控制

多數的版本管理系統都支援了在任意提交點打標籤。這些標籤通常是被用來發布新特性(v1.0.0、v2.3.8,等等),而且一般都是不可變的。

圖中顯示,conf 已經被打了三個不同的版本標籤。這三個標籤遵循著語義化版本的格式。

利用版本管理工具,我們可以通過指定 tag 實現 clone 任意版本的 conf 包的目的。但這有兩個問題亟待解決。

  • 我應該使用哪個版本的包;
  • 我如何才能知道哪個版本的包相容我所寫的或使用的程式碼;

一旦回答完這兩個問題,又會產生第三個問題:

  • 從哪裡下載依賴的程式碼,Go 工具要能查詢和訪問到它;

接著,情況變得更差。

為了要使用特定版本的 conf 包,你必須要下載 conf 的所有依賴。對於所有存在依賴傳遞的專案,這是一個共性的問題。

在 GOPATH 模式下,可以使用 go get 識別和下載所有的依賴包,然後放到 GOPATH 指定的工作區下。但這不是一個完美的方案,因為 go get 僅僅只能從 master 分支下載和更新最新的程式碼。當初期寫程式碼時,從 master 下載程式碼沒什麼問題。但幾個月後,有些依賴可能已經升級了,master 分支的最新程式碼可能已經不再相容你的專案。這是因為你的專案沒有遵守明確的版本管理,任何的升級都可能帶來一個不相容的改變。

在 Module 模式下,通過 go get 下載所有的依賴到一個單一的工作區不再是首選方式。你需要一種方式實現為整個專案中的每個依賴指定一個相容版本。同時,還要支援針對同一個依賴不同主版本的引入,以防止出現一個專案中依賴同一個包的不同主版本。

針對上面的這些問題,社群已經開發了一些解決方案,如 dep,godep,glide 等。但 Go 需要一個整合的解決方案。這個方案通過重用 go.mod 檔案實現按版本維護這些直接和間接依賴。然後,將任何一個版本的依賴當成一個不可變的程式碼包。這個特定版本不可變的程式碼包被稱為一個 Module。

整合解決方案

上圖顯示了倉庫和模組的關係。它顯示瞭如何引用到一個特定版本模組中的包。在這種情況下,在 conf-1.1.0 的程式碼從版本為 0.3.1go-cmp 匯入了 cmp 包。既然,依賴資訊已經在 conf 模組中(儲存在模組檔案中),Go 就可以通過內建的工具集獲取指定版本的模組進行編譯構建。

一旦有了模組,許多便利的工程體驗就體現了出來:

  • 可以向全世界的 Go 開發者提供支援,如 build、retain、authenticate,validate,fetch,cache 等;
  • 在不同的版本管理系統前構建一個代理伺服器,從而實現前面提到的那些支援;
  • 可以驗證一個模組是否被修改過,而不用關心它被構建了多少次,從何處何人手裡獲取,

在這方面是非常值得慶幸地,因為在 Go 1.13 中,Go 團隊已經提供了許多這方面的支援。

總結

這篇文章嘗試為後面討論 Go 模組是什麼以及 Go 團隊如何設計了這個方案打下了基礎。接下來還有一些問題需要討論,比如:

  • 一個特定版本的模組是如何被選擇?
  • 模組檔案是什麼樣的組織結構以及它提供了哪些選項幫助你控制模組的選擇?
  • 模組是如何編譯、獲取和快取到本地的磁碟幫助實現匯入包的解析?
  • 如何通過語義版本進行模組驗證?
  • 如何在你的專案中使用模組以及有什麼最佳實踐?

在接下來的文章中,我計劃將針對這些問題提供一個更深度的理解。現在,你要確保自己已經明白了倉庫、包和模組之間的關係。