Go modules 是 Go 語言中正式官宣的項目依賴解決方案,Go modules(前身爲vgo)於 Go1.11 正式發佈,在 Go1.14 已經準備好,而且能夠用在生產上(ready for production)了,Go官方也鼓勵全部用戶從其餘依賴項管理工具遷移到 Go modules。html
而 Go1.14,在近期也終於正式發佈,Go 官方親自 「喊」 你來用:git
所以在今天這篇文章中,我將給你們帶來 Go modules 的 「終極入門」,歡迎你們一塊兒共同探討。github
Go modules 是 Go 語言中正式官宣的項目依賴管理工具,Go modules(前身爲vgo)於 Go1.11 正式發佈,在 Go1.14 已經準備好,而且能夠用在生產上(ready for production)了,鼓勵全部用戶從其餘依賴項管理工具遷移到 Go modules。golang
Go modules 是 Go 語言的依賴解決方案,發佈於 Go1.11,成長於 Go1.12,豐富於 Go1.13,正式於 Go1.14 推薦在生產上使用。算法
Go moudles 目前集成在 Go 的工具鏈中,只要安裝了 Go,天然而然也就可使用 Go moudles 了,而 Go modules 的出現也解決了在 Go1.11 前的幾個常見爭議問題:緩存
咱們有提到 Go modules 的解決的問題之一就是「淘汰」掉 GOPATH,可是 GOPATH 又是什麼呢,爲何在 Go1.11 前就使用 GOPATH,而 Go1.11 後就開始逐步建議使用 Go modules,再也不推薦 GOPATH 的模式了呢?安全
咱們先看看第一個問題,GOPATH 是什麼,咱們能夠輸入以下命令查看:bash
$ go env GOPATH="/Users/eddycjy/go" ...
咱們輸入go env
命令行後能夠查看到 GOPATH 變量的結果,咱們進入到該目錄下進行查看,以下:app
go ├── bin ├── pkg └── src ├── github.com ├── golang.org ├── google.golang.org ├── gopkg.in ....
GOPATH目錄下一共包含了三個子目錄,分別是:ide
.go
文件或源代碼。在編寫 Go 應用程序,程序包和庫時,通常會以$GOPATH/src/github.com/foo/bar
的路徑進行存放。所以在使用 GOPATH 模式下,咱們須要將應用代碼存放在固定的$GOPATH/src
目錄下,而且若是執行go get
來拉取外部依賴會自動下載並安裝到$GOPATH
目錄下。
在 GOPATH 的 $GOPATH/src
下進行 .go
文件或源代碼的存儲,咱們能夠稱其爲 GOPATH 的模式,這個模式,看起來好像沒有什麼問題,那麼爲何咱們要棄用呢,參見以下緣由:
GOPATH 模式下沒有版本控制的概念,具備致命的缺陷,至少會形成如下問題:
go get
的時候,你沒法傳達任何的版本信息的指望,也就是說你也沒法知道本身當前更新的是哪個版本,也沒法經過指定來拉取本身所指望的具體版本。github.com/foo/bar
。Go1 在 2012 年 03 月 28 日發佈,而 Go1.11 是在 2018 年 08 月 25 日才正式發佈(數據來源:Github Tag),在這個空檔的時間內,並無 Go modules 這一個東西,最先期可能還好說,由於剛發佈,用的人很少,因此沒有明顯暴露,可是後期 Go 語言使用的人愈來愈多了,那怎麼辦?
這時候社區中逐漸的涌現出了大量的依賴解決方案,百花齊放,讓人難以挑選,其中包括咱們所熟知的 vendor 目錄的模式,以及曾經一度被認爲是「官宣」的 dep 的這類依賴管理工具。
但爲何 dep 沒有正在成爲官宣呢,實際上是由於隨着 Russ Cox 與 Go 團隊中的其餘成員不斷深刻地討論,發現dep 的一些細節彷佛愈來愈不適合 Go,所以官方採起了另起 proposal 的方式來推動,其方案的結果一開始先是釋出 vgo(Go modules的前身,知道便可,不須要深刻了解),最終演變爲咱們如今所見到的 Go modules,也在 Go1.11 正式進入了 Go 的工具鏈。
所以與其說是 「在GOPATH模式下的產物」,不如說是歷史爲當前提供了重要的教訓,所以出現了 Go modules。
在初步瞭解了 Go modules 的前世此生後,咱們正式進入到 Go modules 的使用,首先咱們將從頭開始建立一個 Go modules 的項目(原則上所建立的目錄應該不要放在 GOPATH 之中)。
在 Go modules 中,咱們可以使用以下命令進行操做:
命令 | 做用 |
---|---|
go mod init | 生成 go.mod 文件 |
go mod download | 下載 go.mod 文件中指明的全部依賴 |
go mod tidy | 整理現有的依賴 |
go mod graph | 查看現有的依賴結構 |
go mod edit | 編輯 go.mod 文件 |
go mod vendor | 導出項目全部的依賴到vendor目錄 |
go mod verify | 校驗一個模塊是否被篡改過 |
go mod why | 查看爲何須要依賴某模塊 |
在 Go modules 中有以下經常使用環境變量,咱們能夠經過 go env
命令來進行查看,以下:
$ go env GO111MODULE="auto" GOPROXY="https://proxy.golang.org,direct" GONOPROXY="" GOSUMDB="sum.golang.org" GONOSUMDB="" GOPRIVATE="" ...
Go語言提供了 GO111MODULE 這個環境變量來做爲 Go modules 的開關,其容許設置如下參數:
你可能會留意到 GO111MODULE 這個名字比較「奇特」,實際上在 Go 語言中常常會有這類階段性的變量, GO111MODULE 這個命名錶明着Go語言在 1.11 版本添加的,針對 Module 的變量。
像是在 Go1.5 版本的時候,也發佈了一個系統環境變量 GO15VENDOREXPERIMENT,做用是用於開啓 vendor 目錄的支持,當時其默認值也不是開啓,僅僅做爲 experimental。其隨後在 Go1.6 版本時也將默認值改成了開啓,而且最後做爲了official,GO15VENDOREXPERIMENT 系統變量就退出了歷史舞臺。
而將來 GO111MODULE 這一個系統環境變量也會面臨這個問題,也會先調整爲默認值爲 on(曾經在Go1.13想一想改成 on,而且已經合併了 PR,但最後由於種種緣由改回了 auto),而後再把 GO111MODULE 的支持給去掉,咱們猜想應該會在 Go2 將 GO111MODULE 給去掉,由於若是直接去掉 GO111MODULE 的支持,會存在兼容性問題。
這個環境變量主要是用於設置 Go 模塊代理(Go module proxy),其做用是用於使 Go 在後續拉取模塊版本時可以脫離傳統的 VCS 方式,直接經過鏡像站點來快速拉取。
GOPROXY 的默認值是:https://proxy.golang.org,direct
,這有一個很嚴重的問題,就是 proxy.golang.org
在國內是沒法訪問的,所以這會直接卡住你的第一步,因此你必須在開啓 Go modules 的時,同時設置國內的 Go 模塊代理,執行以下命令:
$ go env -w GOPROXY=https://goproxy.cn,direct
GOPROXY的值是一個以英文逗號 「,」 分割的 Go 模塊代理列表,容許設置多個模塊代理,假設你不想使用,也能夠將其設置爲 「off」 ,這將會禁止 Go 在後續操做中使用任何 Go 模塊代理。
而在剛剛設置的值中,咱們能夠發現值列表中有 「direct」 標識,它又有什麼做用呢?
實際上 「direct」 是一個特殊指示符,用於指示 Go 回源到模塊版本的源地址去抓取(好比 GitHub 等),場景以下:當值列表中上一個 Go 模塊代理返回 404 或 410 錯誤時,Go 自動嘗試列表中的下一個,碰見 「direct」 時回源,也就是回到源地址去抓取,而碰見 EOF 時終止並拋出相似 「invalid version: unknown revision...」 的錯誤。
它的值是一個 Go checksum database,用於在拉取模塊版本時(不管是從源站拉取仍是經過 Go module proxy 拉取)保證拉取到的模塊版本數據未通過篡改,若發現不一致,也就是可能存在篡改,將會當即停止。
GOSUMDB的默認值爲:sum.golang.org
,在國內也是沒法訪問的,可是 GOSUMDB 能夠被 Go 模塊代理所代理(詳見:Proxying a Checksum Database)。
所以咱們能夠經過設置 GOPROXY 來解決,而先前咱們所設置的模塊代理 goproxy.cn
就能支持代理 sum.golang.org
,因此這一個問題在設置 GOPROXY 後,你能夠不須要過分關心。
另外若對 GOSUMDB 的值有自定義需求,其支持以下格式:
<SUMDB_NAME>+<PUBLIC_KEY>
。<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>
。也能夠將其設置爲「off」,也就是禁止 Go 在後續操做中校驗模塊版本。
這三個環境變量都是用在當前項目依賴了私有模塊,例如像是你公司的私有 git 倉庫,又或是 github 中的私有庫,都是屬於私有模塊,都是要進行設置的,不然會拉取失敗。
更細緻來說,就是依賴了由 GOPROXY 指定的 Go 模塊代理或由 GOSUMDB 指定 Go checksum database 都沒法訪問到的模塊時的場景。
而通常建議直接設置 GOPRIVATE,它的值將做爲 GONOPROXY 和 GONOSUMDB 的默認值,因此建議的最佳姿式是直接使用 GOPRIVATE。
而且它們的值都是一個以英文逗號 「,」 分割的模塊路徑前綴,也就是能夠設置多個,例如:
$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
設置後,前綴爲 git.xxx.com 和 github.com/eddycjy/mquote 的模塊都會被認爲是私有模塊。
若是不想每次都從新設置,咱們也能夠利用通配符,例如:
$ go env -w GOPRIVATE="*.example.com"
這樣子設置的話,全部模塊路徑爲 example.com 的子域名(例如:git.example.com)都將不通過 Go module proxy 和 Go checksum database,須要注意的是不包括 example.com 自己。
目前Go modules並非默認開啓,所以Go語言提供了GO111MODULE這個環境變量來做爲Go modules的開關,其容許設置如下參數:
若是你不肯定你當前的值是什麼,能夠執行go env
命令,查看結果:
$ go env GO111MODULE="off" ...
若是須要對GO111MODULE的值進行變動,推薦經過go env
命令進行設置:
$ go env -w GO111MODULE=on
可是須要注意的是若是對應的系統環境變量有值了(進行過設置),go env
是不支持覆蓋寫入的,不然會出現以下報錯信息:warning: go env -w GO111MODULE=... does not override conflicting OS environment variable
。
又或是能夠經過直接設置系統環境變量(寫入對應的.bash_profile文件亦可)來實現這個目的:
$ export GO111MODULE=on
在完成 Go modules 的開啓後,咱們須要建立一個示例項目來進行演示,執行以下命令:
$ mkdir -p $HOME/eddycjy/module-repo $ cd $HOME/eddycjy/module-repo
而後進行Go modules的初始化,以下:
$ go mod init github.com/eddycjy/module-repo go: creating new go.mod: module github.com/eddycjy/module-repo
在執行 go mod init
命令時,咱們指定了模塊導入路徑爲 github.com/eddycjy/module-repo
。接下來咱們在該項目根目錄下建立 main.go 文件,以下:
package main import ( "fmt" "github.com/eddycjy/mquote" ) func main() { fmt.Println(mquote.GetHello()) }
而後在項目根目錄執行 go get github.com/eddycjy/mquote
命令,以下:
$ go get github.com/eddycjy/mquote go: finding github.com/eddycjy/mquote latest go: downloading github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f go: extracting github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f
在初始化項目時,會生成一個 go.mod 文件,是啓用了 Go modules 項目所必須的最重要的標識,同時也是GO111MODULE 值爲 auto 時的識別標識,它描述了當前項目(也就是當前模塊)的元信息,每一行都以一個動詞開頭。
在咱們剛剛進行了初始化和簡單拉取後,咱們再次查看go.mod文件,基本內容以下:
module github.com/eddycjy/module-repo go 1.13 require ( github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f )
爲了更進一步的講解,咱們模擬引用以下:
module github.com/eddycjy/module-repo go 1.13 require ( example.com/apple v0.1.2 example.com/banana v1.2.3 example.com/banana/v2 v2.3.4 example.com/pear // indirect example.com/strawberry // incompatible ) exclude example.com/banana v1.2.4 replace example.com/apple v0.1.2 => example.com/fried v0.1.0 replace example.com/banana => example.com/fish
另外你會發現 example.com/pear
的後面會有一個 indirect 標識,indirect 標識表示該模塊爲間接依賴,也就是在當前應用程序中的 import 語句中,並無發現這個模塊的明確引用,有多是你先手動 go get
拉取下來的,也有多是你所依賴的模塊所依賴的,狀況有好幾種。
在第一次拉取模塊依賴後,會發現多出了一個 go.sum 文件,其詳細羅列了當前項目直接或間接依賴的全部模塊版本,並寫明瞭那些模塊版本的 SHA-256 哈希值以備 Go 在從此的操做中保證項目所依賴的那些模塊版本不會被篡改。
github.com/eddycjy/mquote v0.0.1 h1:4QHXKo7J8a6J/k8UA6CiHhswJQs0sm2foAQQUq8GFHM= github.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg= github.com/eddycjy/mquote/module/tour v0.0.1 h1:cc+pgV0LnR8Fhou0zNHughT7IbSnLvfUZ+X3fvshrv8= github.com/eddycjy/mquote/module/tour v0.0.1/go.mod h1:8uL1FOiQJZ4/1hzqQ5mv4Sm7nJcwYu41F3nZmkiWx5I= ...
咱們能夠看到一個模塊路徑可能有以下兩種:
github.com/eddycjy/mquote v0.0.1 h1:4QHXKo7J8a6J/k8UA6CiHhswJQs0sm2foAQQUq8GFHM= github.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg=
h1 hash 是 Go modules 將目標模塊版本的 zip 文件開包後,針對全部包內文件依次進行 hash,而後再把它們的 hash 結果按照固定格式和算法組成總的 hash 值。
而 h1 hash 和 go.mod hash 二者,要不就是同時存在,要不就是隻存在 go.mod hash。那什麼狀況下會不存在 h1 hash 呢,就是當 Go 認爲確定用不到某個模塊版本的時候就會省略它的 h1 hash,就會出現不存在 h1 hash,只存在 go.mod hash 的狀況。
咱們剛剛成功的將 github.com/eddycjy/mquote
模塊拉取了下來,其拉取的結果緩存在 $GOPATH/pkg/mod
和 $GOPATH/pkg/sumdb
目錄下,而在mod
目錄下會以 github.com/foo/bar
的格式進行存放,以下:
mod ├── cache ├── github.com ├── golang.org ├── google.golang.org ├── gopkg.in ...
須要注意的是同一個模塊版本的數據只緩存一份,全部其它模塊共享使用。若是你但願清理全部已緩存的模塊版本數據,能夠執行 go clean -modcache
命令。
在拉取項目依賴時,你會發現拉取的過程總共分爲了三大步,分別是 finding(發現)、downloading(下載)以及 extracting(提取), 而且在拉取信息上一共分爲了三段內容:
須要注意的是,所拉取版本的 commit 時間是以UTC時區爲準,而並不是本地時區,同時咱們會發現咱們 go get
命令所拉取到的版本是 v0.0.0,這是由於咱們是直接執行 go get -u
獲取的,並無指定任何的版本信息,由 Go modules 自行按照內部規則進行選擇。
剛剛咱們用 go get
命令拉取了新的依賴,那麼 go get
又提供了哪些功能呢,經常使用的拉取命令以下:
命令 | 做用 |
---|---|
go get | 拉取依賴,會進行指定性拉取(更新),並不會更新所依賴的其它模塊。 |
go get -u | 更新現有的依賴,會強制更新它所依賴的其它所有模塊,不包括自身。 |
go get -u -t ./... | 更新全部直接依賴和間接依賴的模塊版本,包括單元測試中用到的。 |
那麼我想選擇具體版本應當如何執行呢,以下:
命令 | 做用 |
---|---|
go get golang.org/x/text@latest | 拉取最新的版本,若存在tag,則優先使用。 |
go get golang.org/x/text@master | 拉取 master 分支的最新 commit。 |
go get golang.org/x/text@v0.3.2 | 拉取 tag 爲 v0.3.2 的 commit。 |
go get golang.org/x/text@342b2e | 拉取 hash 爲 342b231 的 commit,最終會被轉換爲 v0.3.2。 |
咱們回顧一下咱們拉取的 go get github.com/eddycjy/mquote
,其結果是 v0.0.0-20200220041913-e066a990ce6f
,對照着上面所提到的 go get
行爲來看,你可能還會有一些疑惑,那就是在 go get
沒有指定任何版本的狀況下,它的版本選擇規則是怎麼樣的,也就是爲何 go get
拉取的是 v0.0.0
,它何時會拉取正常帶版本號的 tags 呢。實際上這須要區分兩種狀況,以下:
所拉取的模塊有發佈 tags:
所拉取的模塊沒有發佈過 tags:
那麼爲何會拉取的是 v0.0.0
呢,是由於 github.com/eddycjy/mquote
沒有發佈任何的tag,以下:
所以它默認取的是主分支最新一次 commit 的 commit 時間和 commithash,也就是 20200220041913-e066a990ce6f
,屬於第二種狀況。
在項目有發佈 tags 的狀況下,還存在着多種模式,也就是隻有單個模塊和多個模塊,咱們統一以多個模塊來進行展現,由於多個模塊的狀況下就已經包含了單個模塊的使用了,以下圖:
在這個項目中,咱們一共打了兩個tag,分別是:v0.0.1 和 module/tour/v0.0.1。這時候你可能會奇怪,爲何要打 module/tour/v0.0.1
這麼「奇怪」的tag,這有什麼用意嗎?
實際上是 Go modules 在同一個項目下多個模塊的tag表現方式,其主要目錄結構爲:
mquote ├── go.mod ├── module │ └── tour │ ├── go.mod │ └── tour.go └── quote.go
能夠看到在 mquote
這個項目的根目錄有一個 go.mod 文件,而在 module/tour
目錄下也有一個 go.mod 文件,其模塊導入和版本信息的對應關係以下:
tag | 模塊導入路徑 | 含義 |
---|---|---|
v0.0.1 | github.com/eddycjy/mquote | mquote 項目的v 0.0.1 版本 |
module/tour/v0.01 | github.com/eddycjy/mquote/module/tour | mquote 項目下的子模塊 module/tour 的 v0.0.1 版本 |
結合上述內容,拉取主模塊的話,仍是照舊執行以下命令:
$ go get github.com/eddycjy/mquote@v0.0.1 go: finding github.com/eddycjy/mquote v0.0.1 go: downloading github.com/eddycjy/mquote v0.0.1 go: extracting github.com/eddycjy/mquote v0.0.1
若是是想拉取子模塊,執行以下命令:
$ go get github.com/eddycjy/mquote/module/tour@v0.0.1 go: finding github.com/eddycjy/mquote/module v0.0.1 go: finding github.com/eddycjy/mquote/module/tour v0.0.1 go: downloading github.com/eddycjy/mquote/module/tour v0.0.1 go: extracting github.com/eddycjy/mquote/module/tour v0.0.1
咱們將主模塊和子模塊的拉取進行對比,你會發現子模塊的拉取會多出一步,它會先發現 github.com/eddycjy/mquote/module
,再繼續推算,最終拉取到 module/tour
。
在前面的模塊拉取和引用中,你會發現咱們的模塊導入路徑就是 github.com/eddycjy/mquote
和 github.com/eddycjy/mquote/module/tour
,彷佛並無什麼特殊的。
其實否則,實際上 Go modules 在主版本號爲 v0 和 v1 的狀況下省略了版本號,而在主版本號爲v2及以上則須要明確指定出主版本號,不然會出現衝突,其tag與模塊導入路徑的大體對應關係以下:
tag | 模塊導入路徑 |
---|---|
v0.0.0 | github.com/eddycjy/mquote |
v1.0.0 | github.com/eddycjy/mquote |
v2.0.0 | github.com/eddycjy/mquote/v2 |
v3.0.0 | github.com/eddycjy/mquote/v3 |
簡單來說,就是主版本號爲 v0 和 v1 時,不須要在模塊導入路徑包含主版本的信息,而在 v1 版本之後,也就是 v2 起,必需要在模塊的導入路徑末尾加上主版本號,引用時就須要調整爲以下格式:
import ( "github.com/eddycjy/mquote/v2/example" )
另外忽略主版本號 v0 和 v1 是強制性的(不是可選項),所以每一個軟件包只有一個明確且規範的導入路徑。
咱們不斷地在 Go Modules 的使用中提到版本號,其實質上被稱爲「語義化版本」,假設咱們的版本號是 v1.2.3,以下:
其版本格式爲「主版本號.次版本號.修訂號」,版本號的遞增規則以下:
假設你是先行版本號或特殊狀況,能夠將版本信息追加到「主版本號.次版本號.修訂號」的後面,做爲延伸,以下:
至此咱們介紹了 Go modules 所支持的兩類版本號方式,在咱們發佈新版本打 tag 的時候,須要注意遵循,不然不遵循語義化版本規則的版本號都是沒法進行拉取的。
如今咱們已經有一個模塊,也有發佈的 tag,可是一個模塊每每依賴着許多其它許許多多的模塊,而且不一樣的模塊在依賴時頗有可能會出現依賴同一個模塊的不一樣版本,以下圖(來自Russ Cox):
在上述依賴中,模塊 A 依賴了模塊 B 和模塊 C,而模塊 B 依賴了模塊 D,模塊 C 依賴了模塊 D 和 F,模塊 D 又依賴了模塊 E,並且同模塊的不一樣版本還依賴了對應模塊的不一樣版本。那麼這個時候 Go modules 怎麼選擇版本,選擇的是哪個版本呢?
咱們根據 proposal 可得知,Go modules 會把每一個模塊的依賴版本清單都整理出來,最終獲得一個構建清單,以下圖(來自Russ Cox):
咱們看到 rough list 和 final list,二者的區別在於重複引用的模塊 D(v1.三、v1.4),其最終清單選用了模塊 D 的 v1.4 版本,主要緣由:
理論上 go.mod 和 go.sum 文件都應該提交到你的 Git 倉庫中去。
假設咱們不上傳 go.sum 文件,就會形成每一個人執行 Go modules 相關命令,又會生成新的一份 go.sum,也就是會從新到上游拉取,再拉取時有可能就是被篡改過的了,會有很大的安全隱患,失去了與基準版本(第一個所提交的人,所指望的版本)的校驗內容,所以 go.sum文件是須要提交。
至此咱們介紹了 Go modules 的前世此生、基本使用和在 Go modules 模式下 go get
命令的行爲轉換,同時咱們對常見的多版本導入路徑、語義化版本控制以及多模塊的最小版本選擇規則進行了大體的介紹。
Go modules 的成長和發展經歷了必定的過程,若是你是剛接觸的讀者,直接基於 Go modules 的項目開始便可,若是既有老項目,那麼是時候考慮切換過來了,Go1.14起已經準備就緒,並推薦你使用。