手把手教你如何創建及使用 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 >

根據讀取到的 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 的集合。 其要實現的目標如下:

一個 module 也是可以像 package 一樣共享的。因此,module 也必須是一個 git 倉庫或其他 Go 可支持的代碼控制系統。因此,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