手把手教你如何創建及使用 Go module
Go module 是從 Go 1.11 版本才引入的新功能。其目標是取代舊的的基於 GOPATH 方法來指定在工程中使用哪些源文件或導入包。本文首先分析 Go 引入 module 之前管理依賴的優缺點,然後針對這些缺點,看 module 是如何解決的。
一、傳統的包管理方式 - package
在 Go1.11 之前,如果想要編寫 Go 代碼以及引入第三方包,則需要將源代碼寫在 GOPATH/src 目錄下。即開發者只能將研發的項目放到 GOPATH 目錄下。同時,將引入的第三方包會下載到 GOPATH/pkg 目錄下。我們先來看下在這種包管理模式下,使用 go get 是如何安裝依賴包的,然後再分析這種包管理的不足。
1.1 go get 的工作流程
我們以在項目中引入 github.com/go-redis/redis 包爲例。在項目中使用 import 導入該包:
import "github.com/go-redis/redis"
然後我們需要使用 go get 命令將該包下載下來:
go get github.com/go-redis/redis
運行 go get 命令後,Go 會訪問 https://github.com/go-redis/redis 並下載該包。一旦下載完成,該包就會被保存到 $GOPATH/pkg/github.com/go-redis/redis 目錄下。
那麼從執行 go get 命令到包被保存到對應的目錄期間,go get 都經歷了哪些過程呢?
首先,Go 會將包拼接成 https 協議的 URL 地址。這裏是 https://github.com/go-redis/redis 。Go 的第三方包是存儲在像 GIT 或 SVN 這樣的在線版本控制管理系統上的。Go 目前支持的在線版本管理類型如下:
Bazaar .bzr
Fossil .fossil
Git .git
Mercurial .hg
Subversion .svn
所以,在示例中,Go 首先會解析 github.com/go-redis/redis.git (模板格式:github.com/go-redis/redis{.type})。
其次,根據支持的協議依次嘗試 clone 該包。若該在線版本管理系統支持多種協議,那麼 Go 會依次嘗試。例如,Git 支持 https:// 和 git+ssh:// 協議 , 那麼 Go 會依次使用對應的協議進行解析該包。如果 Go 成功解析了對應的 URL 地址,那麼該包將會被 clone 並保存到 $GOPATH/pkg 目錄下。
最後,若版本管理系統不是 Go 所支持的,則嘗試查找 META 信息。在這種場景下,Go 也會試圖使用 https 或 http 協議拼裝成的 URL 地址去解析。並從返回的 HTML 代碼中查找 META 信息:
<meta >
-
import-prefix: 這是模塊所導入的路徑。在我們的示例中是 github.com/go-redis/go
-
type: 在線版本管理系統的類型。可以是上面我們提到的 Go 支持的類型之一。在我們的示例中是 git。
-
repo-root: 代碼倉庫在版本控制系統中的根 URL 地址。例如,在我們的示例中,應該是 https://github.com/go-redis/redis.git。
根據讀取到的 meta 信息,Go 就可以從 https://github.com/go-redis/redis.git 中克隆該項目代碼,並將其保存到本地的 $GOPATH/src 目錄下的 github.com/go-redis/redis 中。
到此,我們已經瞭解了傳統的包管理的工作方式了。下面我們來看看這種管理方式有哪些缺點。
1.2 傳統包管理方式的不足
首先,所有的項目都必須在 GOPATH/src 指向的目錄下,或者必須更改 GOPATH 環境變量所指向的目錄。
我們以兩個項目 A、B 來舉例說明。假設當前的 GOPATH=/usr/local/goworkspace/。如果保持 GOPATH 不變的話,那麼 A、B 兩個項目的源代碼都必須要放到 GOPATH 的目錄下,即 / usr/local/goworkspace/src 目錄下。同時,A 和 B 項目引入的第三方包都會在 GOPATH/pkg 目錄下。這樣兩個項目其實就是混合在一起。
如果不想混合在一起怎麼辦呢?那就只能更改 GOPATH 的目錄。假設我們現在在研發 A 項目,並將其工作目錄放在 / usr/local/goworkspace/a 目錄下,GOPATH=/usr/local/goworkspace/a。但是在開發 B 項目時,更改 GOPATH 的指向,例如我們這裏使用 / usr/local/goworkspace/b 目錄下。這樣兩個項目的源代碼以及依賴的第三方包就在各自項目下了。但同時如果想繼續修改 A 項目的代碼時,就需要再將 GOPATH 目錄更改到指向 A 項目的目錄中,即 GOPATH=/usr/local/goworkspace/src 目錄。
其次,對於依賴的同一個包只能從 master 分支上導入最新的提交,且不能導入包的指定的版本。
假設我們有一個第三方包 redis,項目 A 首次引入該包時,使用 go get 命令從代碼庫的 master 分支下載當前最新的代碼,並將該包保存在本地的 GOPATH/pkg 目錄下。之後 redis 包有了新的提交,但同時也引入了一個 bug。如果項目 A 升級或重新安裝該包時,使用 go get 命令並沒有指定特定版本的參數,還是從該包的代碼庫的 master 分支中下載該包,也就造成了向後不兼容。另外,升級或重新安裝的包也會被安裝到 GOPATH/pkg 下的相同目錄,因爲沒有版本的管理,所以會覆蓋之前。
好了,以上就是在傳統的包管理方式中的兩大主要不足之處。那麼針對這些不足,我們來看看 Go 的 module 是如何解決的。
二、現代包管理方式 - module
2.1 什麼是 module
一個 module 就是一個包含多個 package 的目錄,即一個 package 的集合。 其要實現的目標如下:
-
首先,研發者應該能夠在任何目錄下工作,而不僅僅是在 GOPATH 指定的目錄。
-
可以安裝依賴包的指定版本,而不是隻能從 master 分支安裝最新的版本。
-
可以導入同一個依賴包的多個版本。當我們老項目使用老版本,新項目使用新版本時會非常有用。
-
要有一個能夠羅列當前項目所依賴包的列表。這個的好處是當我們發佈項目時不用同時發佈所依賴的包。Go 能夠根據該文件自動下載對應的包。
一個 module 也是可以像 package 一樣共享的。因此,module 也必須是一個 git 倉庫或其他 Go 可支持的代碼控制系統。因此,Go 的建議是:
-
一個 module 必須是一個代碼控制系統的倉庫,並且一個倉庫應該只能包含一個 module。
-
一個 module 應該包含一個或多個 package。
-
一個包應該在同一個目錄下包含一個或多個 go 文件
2.2 如何創建 module
第一,我們在 GOPATH 之外的任何位置創建一個目錄。
這裏我們使用 encodex,該 encodex 包含一些對字符串的編碼功能函數,例如 md5,sha1 等。如下圖:
根據上面所討論的,一個 Go module 應該是一個版本控制系統上的代碼倉庫。所以我們在 github 上創建一個 git 的代碼倉庫,如下圖:
第二,在本地的目錄下執行 go mod init 命令來初始化 Go module。
go mod init github.com/goxuetang/encodex
該命令會在 encodex 的根目錄下創建 go.mod 文件,go.mod 文件會包含我們定義的 module 的導入路徑和依賴的包及對應的版本。如下所示:
由上圖可知,在生成的 go.mod 文件中顯示了該 module 可被導入的路徑以及 Go 的版本。因爲目前還沒有導入任何其他依賴包,所以沒有顯示導入包的信息。好,現在我們把該目錄同時提交到 git 上。
git init
git remote add origin https://github.com/goxuetang/encodex.git
第三,我們在 encodex 的 hash 包中添加如下代碼:
好了,到這裏我們就可以發佈我們的包。但在發佈之前我們先來看下語義化的版本。
語義化的版本是一種通用的版本格式。其格式如下:
vMajor.Minor.Patch
該格式以固定的字母 v 開頭,Major 代表主版本,Minor 代表次版本,Patch 代表不定版本。只有在版本不兼容之前的版本時,纔會改動主版本 Major。當做了向下兼容的功能時會改動 Minor。當對次版本 Minor 做了問題修正時會改動 Patch。詳細的語義化版本可參考語義化版本官方文檔進一步閱讀。
Go 語言指出,當一個 module 的新老版本不兼容時,新版本應該發佈一個新的主版本。同時,Go 會認爲這是一個獨立的 module,和之前的老版本沒有任何關係。
Git 的分支本質上是一個歷史提交的記錄。對於每一次提交都有一個唯一的標識對應。對於每一個唯一標識,我們還可以給一個語義化的版本別名,也就是我們所說的 tag。
最後,我們可以給我們的 module 打一個 tag 了。
因爲是第一個版本,所以我們使用版本 v1.0.0,如下:
git tag v1.0.0
git push --tags
到此,我們的 module 已經發布了,並由一個 v1.0.0 的 tag 版本。接下來,我們看看在項目中如何使用該 module
2.4 如何使用第三方 module
我們在新建的 main module 中創建了一個 main.go 文件,在該 module 下要想使用 encodex 模塊下的包,則需要引入和安裝兩個步驟。在文件中使用 import 語句引入包,如下圖:
第一步,使用 import 引入模塊下具體的包。因爲在 encodex 的 module 中,我們設置的引入路徑是 github.com/goxuetang/encodex, 即 go.mod 文件的第一行。hash 包是 encodex 模塊下的一個包。所以我們引入的完整路徑是:
import "github.com/goxuetang/encodex/hash"
第二步,使用 go get 命令安裝引入的包。使用 go get 命令時,可以指定包的具體版本,如下:
go get github.com/goxuetang/encodex/hash@v1.0.0
也可以不指定版本,這時 go get 命令會自動的查找最近的版本,如下:
go get github.com/goxuetang/encodex/hash
go get:added github.com/goxuetang/encodex v1.0.0
如圖所示:
同時,go get 會將引入的包加在 go.mod 文件中。require 中不僅有包名,還有對應的版本號。如下圖所示:
好,我們現在來看另外一個問題,下載下來的包存在哪裏了。
2.5 module 存儲在哪裏
當 go get 將包下載下來後,會將其存儲到 GOPATH/pkg/mod 目錄下。通過 go env 可以查看 GOPATH 環境變量的具體指向目錄,我的環境下的 GOPATH=/Users/YuYang/go,如下是上節中引入的 encodex 模塊。如下圖所示:
我們發現 encodex 模塊的目錄是帶版本號的,這也是 Go module 能夠支持多版本的原因。
三、如何升級版本
在上面我們有講到 module 使用的是 vX.X.X 格式的語義化版本。那麼在日常的研發中又是如何對這三個版本號進行升級的呢。
3.1 如何升級 module 的小版本和補丁版本
隨着時間的推移,發佈的包肯定會有新的提交,比如修復了一個 bug,則 patch 版本號會升級,添加了一個新功能,則小版本號會升級。做了一項大的改動,和前一個版本不兼容了,那麼主版本號就會升級。接下來我們看看在已引入的包後,如何升級對應的版本。
如果我們只想升級補丁版本 patch,那麼可以使用如下命令:
go get -u=patch
如果想更新同一個大版本下的小版本,那麼可以使用如下命令:
go get -u
該命令是如果小版本有更新,則升級小版本。如果只有補丁版本有更新,則會升級補丁版本。
如果想升級到指定的版本,則使用指定版本的命令:
go get module@version
例如,要將 encodex 模塊升級到 v1.1.3 版本,則使用如下命令:
go get github.com/goxuetang/encodex@v1.1.3
3.2 如何升級 module 的大版本
如果想要升級大版本則需要重新安裝大版本,因爲在上面我們有提到,在 Go 中,會將一個大版本視爲一個全新的模塊。因此,需要使用 go get 安裝該大版本的模塊,同時在對應的文件中通過 import 引入該包。例如 encodex 模塊升級到了 v2 版本,那麼就需要在 encodex 模塊的 go.mod 中將導入路徑更改爲 v2。如下:
github.com/goxuetang/encodex/v2
然後就可以在工程中引用該 v2 版本的模塊了。如下:
import newHash github.com/goxuetang/encodex/v2/hash
同時使用 go get 命令下載並安裝該模塊:
go get github.com/goxuetang/encodex/v2
四、間接依賴
一個工程所依賴的模塊可分爲直接依賴和 ** 間接依賴** 。直接依賴就是我們的工程文件中使用 import 語句導入的模塊。而間接依賴就是我們直接依賴的模塊所依賴的。如下圖:
現在我們在 main 模塊中引入 github.com/go-redis/redis 模塊,然後查看 go.mod 文件,發現有如下間接的依賴模塊,這裏的模塊正是在 github.com/go-redis/redis 中引入的模塊,可以查看 github.com/go-redis/redis 模塊的 go.mod 文件以確認。
在上圖中,我們還發現 redis 的模塊後面的版本是 v6.15.9+incompatible。這個代表什麼意思呢?這個代表的是引入的模塊的最新版本是 v5.15.9,但同時具有不兼容的風險。爲什麼呢?因爲在 redis 模塊中未使用規範的導入名稱。例如,規範的模塊命名應該是在模塊的版本大於 1 的時候,導入名稱就需要增加主版本信息。例如,當該模塊是第一個版本時,其對應的 go.mod 文件如下:
module github.com/go-redis/redis
當主版本升級到 2 時,則 go.mod 中的模塊導入名稱應該爲:
module github.com/go-redis/redis/v2
如果不增加 v2 這個標識,那麼當使用 go get github.com/go-redis/redis 下載包的時候,go 會找到模塊名稱沒有使用主版本標識的最新的版本。我們通過查看該模塊在 git 上的 6.15.9 的版本源碼,發現其源碼中並沒有 go.mod 文件。
所以,當模塊的 go.mod 文件中的導入路徑沒有版本後綴(例如 v2)的情況下,默認是 v1 版本,因此在使用 go get 獲取這樣的模塊時,默認會獲取 v1.x.x 的最新版本。
五、 小版本的選擇
我們已經知道了 Go 可以同時導入主版本不同的 module。那麼,如果只有小版本或補丁版本不同,那麼 Go 該如何選擇呢?假設工程項目直接依賴於兩個 module:A 和 B。同時 A 依賴於 MODULE 1 的 v1.0.1 版本,但 B 依賴於 MODULE 1 的 v1.0.2 版本。如下圖所示:
那麼,在工程項目模塊(PROJECT MODULE)中需要間接依賴 MODULE 1 的哪個版本呢?如果我們使用 v1.0.1,那麼 MODULE B 有可能會產生異常。在語義化版本中,我們知道小版本或補丁版本應該向後兼容,即 v1.0.2 是兼容 v1.0.1 的,所以在 PROJECZT MODULE 中應該選擇 MODULE 1 的 v1.0.2 版本。
總結
Go module 不僅解決了項目代碼不再依賴於 GOPATH 路徑,而且還解決了相同 module 的多版本引入問題。通過本篇文章,相信您對 module 的創建、發佈、版本管理、依賴關係都會有了一個清晰的認識。
歡迎關注「Go 學堂」,讓知識活起來
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/JpE5aIl2Lu0T1mEwksKw_Q