您好,登錄后才能下訂單哦!
前言
在上文中,我們介紹了gopath
的含義、功能、優劣、以及如何通過GOPATH來組織項目
在本文中,我們將介紹go module
的原理和用法以試圖能夠回答下面的幾個問題
go module 是什么?
go module為什么需要?
go module的基本使用方法是什么?
go module如何管理版本與依賴?
go module如何解決依賴的沖突問題?
go module 環境變量的配置與使用方式?
如何搭建私有 module鏡像?
在go module
之前,有一些問題長期困擾go語言的開發人員
能否將go工程代碼脫離gopath
之外
能否處理版本依賴問題并且自動選擇最兼容的依賴版本
能否使用go工具本地管理依賴項,自定義依賴項
go1.11開始支持,go1.13全面支持的go modules
正是為了解決上面的問題誕生的,下面我們詳細介紹go module
企圖解決的問題
在介紹gopath
時,我們介紹了如果導入為
import "github.com/gobuffalo/buffalo"
實際引用的是$GOPATH/src/github.com/gobuffalo/buffalo
文件中的代碼。
也就是說,在gopath
中 ,導入路徑與項目在文件系統中的目錄結構和名稱必須是匹配的。
那么能否import
路徑為github.com/gobuffalo/buffalo
,但是項目實際的路徑卻是在另一個任意的文件目錄中?(例如/users/gobuffalo/buffalo
).答案是肯定的,go module
通過在一個特殊的叫做go.mod
的文件中指定模塊名來解決這一問題。
## go.mod
01 module github.com/gobuffalo/buffalo
02
...
06
在go.mod文件的第一行指定了模塊名,模塊名表示開發人員可以用此來引用當前代碼倉庫中任何package
的路徑名,以此來替代$gopath
的路徑。從而,代碼倉庫在任何位置都已經沒有關系,因為Go工具可以使用模塊文件的位置和模塊名來解析代碼倉庫中的任何內部import
。
對于任何版本控制(VCS)工具,我們都能在任何代碼提交點打上"tag"標記,如下所示:
使用VCS工具,開發人員可以通過引用特定標簽將軟件包的任何特定版本克隆到本地。
當我們引用一個第三方包時,可能并不總是希望應用項目最新的代碼,而是某一個特定與當前項目兼容的代碼。對于某一個項目來說,可能并沒有意識到有人在使用他們的代碼,或者某種原因進行了巨大的不兼容更新。
我們希望能夠指明需要使用的第三方包的版本,并且go工具能夠方便下載、管理
更棘手的是,一個第三方包A可能引用了其他的第三方包B,因此還必須把第三方包A的全部依賴下載
如何查找并把所有的依賴包下載下來?
某一個包下載失敗應該怎么辦?
所有項目之間如何進行依賴的傳導?
如何選擇一個最兼容的包?
如何解決包的沖突?
如果希望在項目中同時引用第三方包的二個不同版本,需要如何處理?
因此,只通過gopath
維護單一的master包的方式是遠遠不夠的,因為依賴包的最新代碼不一定與項目兼容。盡管go社區已經針對以上問題提供了一些解決方案(例如dep,godep,glide等)但是go官方的go moudle
提供了一種集成解決方案,通過在文件中維護直接和間接依賴項的版本列表來解決這一問題。通過將一個特定版本的依賴項看做是捆綁的不可變的依賴項,就叫做一個模塊(moudle)
為了加快構建程序的速度并快速切換、獲取項目中依賴項的更新,Go維護了下載到本地計算機上的所有模塊的緩存,緩存目前默認位于$GOPATH/pkg
目錄中。有go的提議希望能夠自定義緩存的位置。
所在位置看上去如下所示:
go/
├── bin
├── pkg
├── darwin_amd64
└── mod
└── src
在mod目錄下,我們能夠看到模塊名路徑中的第一部分用作了模塊緩存中的頂級文件夾
~/go/pkg/mod ? ls -l jackson@192
drwxr-xr-x 6 jackson staff 192 1 15 20:50 cache
drwxr-xr-x 7 jackson staff 224 2 20 17:50 cloud.google.com
drwxr-xr-x 3 jackson staff 96 2 18 12:03 git.apache.org
drwxr-xr-x 327 jackson staff 10464 2 28 00:02 github.com
drwxr-xr-x 8 jackson staff 256 2 20 17:27 gitlab.followme.com
drwxr-xr-x 6 jackson staff 192 2 19 22:05 go.etcd.io
...
當我們打開一個實際的模塊,例如github.com/nats-io
,我們會看到與nats庫有關許多模塊
~/go/pkg/mod ? ls -l github.com/nats-io jackson@192
total 0
dr-x------ 24 jackson staff 768 1 17 10:27 gnatsd@v1.4.1
dr-x------ 15 jackson staff 480 2 17 22:22 go-nats-streaming@v0.4.0
dr-x------ 26 jackson staff 832 2 19 22:05 go-nats@v1.7.0
dr-x------ 26 jackson staff 832 1 17 10:27 go-nats@v1.7.2
...
為了擁有一個干凈的工作環境,我們可以用如下代碼清空緩存區。但是請注意,在正常的工作流程中,是不需要執行如下代碼的。
$ go clean -modcache
我們從GOPATH
外開始一個新的項目講解,新建一個新建夾以及一個main
文件
$ cd $HOME
$ mkdir mathlib
$ cd mathlib jackson@192
$ touch main.go
接著在當前目錄中,執行如下指令初始化moudle。
~/mathlib ? go mod init github.com/dreamerjackson/mathlib
go mod init
指令的功能很簡單,自動生成一個go.mod
文件 后面緊跟的路徑即是自定義的模塊名。習慣上以托管代碼倉庫的URL為模塊名(代碼將會放置在https://github.com/dreamerjackson/mathlib
下)
go.mod
文件 位于項目的根目錄下,內容如下所示,第一行即為模塊名。
module github.com/ardanlabs/service
#### 引入第三方模塊
go 1.13
接下來我們將書寫初始化的代碼片段
package main
import "github.com/dreamerjackson/mydiv"
func main(){
}
我們在代碼片段中導入了為了講解go moudle
而特地的引入的packagegithub.com/dreamerjackson/mydiv
,其進行簡單的除法操作,同時又引入了另一個包github.com/pkg/errors
。其代碼如下圖所示:
如下圖所示,在goland中我們可以看到導入的package 是紅色的,因為此時在go module的緩存并不能找到此package。
為了能夠將此package下載到本地,我們可以使用go mod tidy
指令
$ go mod tidy
go: finding github.com/dreamerjackson/mydiv latest
go: downloading github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161
go: extracting github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161
同時我們在go.mod
中能夠看到新增加了一行用于表示我們引用的依賴關系
module github.com/dreamerjackson/mathlib
go 1.13
require github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161
注意在這里間接的依賴(即github.com/dreamerjackson/mydiv
依賴的github.com/pkg/errors
)并沒有也沒有必要在go.mod
文件展示出來,而是出現在了一個自動生成的新的文件go.sum
中.
## go.sum
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161 h2:QR1fJ05yjzJ0qv1gcUS+gAe5Q3UU5Y0le6TIb2pcJpQ=
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161/go.mod h2:h70Xf3RkhKSNbUF8W3htLNJskYJSITf6AdEGK22QksQ=
github.com/pkg/errors v0.9.1 h2:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h2:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
接著就可以愉快的調用我們的代碼了
package main
import (
"fmt"
"github.com/dreamerjackson/mydiv"
)
func main(){
res,_ :=mydiv.Div(4,2)
fmt.Println(res)
}
運行go run
命令后,即會為我們輸出除法結果2
假設我們依賴的第三方包出現了更新怎么辦?如果將依賴代碼更新到最新的版本呢?
有多種方式可以實現依賴模塊的更新,在go.mod
文件中修改版本號為:
require github.com/dreamerjackson/mydiv latest
或者
require github.com/dreamerjackson/mydiv master
獲取復制commitId 到最后
require github.com/dreamerjackson/mydiv c9a7ffa8112626ba6c85619d7fd98122dd49f850
還有一種辦法是在終端當前項目中,運行go get
go get github.com/dreamerjackson/mydiv
上述幾種方式在保存文件后,再次運行go mod tidy
即會進行更新
此時如果我們再次打開go.sum
文件會發現,go.sum
中不僅僅存儲了直接和間接的依賴,還會存儲過去的版本信息。
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161 h2:QR1fJ05yjzJ0qv1gcUS+gAe5Q3UU5Y0le6TIb2pcJpQ=
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161/go.mod h2:h70Xf3RkhKSNbUF8W3htLNJskYJSITf6AdEGK22QksQ=
github.com/dreamerjackson/mydiv v0.0.0-20200305090126-c9a7ffa81126/go.mod h2:h70Xf3RkhKSNbUF8W3htLNJskYJSITf6AdEGK22QksQ=
github.com/pkg/errors v0.9.1 h2:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h2:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
當我們不想在使用此第三方包時,可以直接在代碼中刪除無用的代碼,接著執行
$ go mod tidy
會發現go.mod
與go.sum
一切又都空空如也~
每個依賴管理解決方案都必須解決選擇依賴版本的問題,當今存在的許多版本選擇算法都試圖識別任何依賴的“最新最大”版本。如果您認為語義版本控制被正確應用并且將遵守約定,那么這是有道理的。在這些情況下,依賴項的“最新最大”版本應該是最穩定和安全的版本,并且應與較早版本具有向后兼容性。
Go決定采用其他方法,Russ Cox花費了大量時間和精力撰寫和談論 Go團隊的版本選擇方法,即最小版本選擇(Minimal Version Selection,MVS)。從本質上講,Go團隊相信MVS可以為Go程序提供最佳的機會,以實現兼容性和可重復性。我建議閱讀這篇文章,以了解Go團隊為什么相信這一點。
go最小版本選擇指的是選擇項目中最合適的最小版本。并不是說MVS不能選擇最新的版本,而是如果項目中任何依賴不需要最新的版本,則不需要它。
舉一個簡單的例子,假設現在項目github.com/dreamerjackson/mydiv
的最新版本為v1.0.2
,可通過下面指令查看所有
> go list -m -versions github.com/dreamerjackson/mydiv
github.com/dreamerjackson/mydiv v1.0.0 v1.0.1 v1.0.2 v1.0.3
假設現在有兩個模塊A、B,都依賴模塊D。其中
A -> D v1.0.1,
B -> D v1.0.2
如果我們的當前項目只依賴A,這個時候go module
會如何選擇呢?像dep這樣的依賴工具將選擇v1.0.3,即最新的語義版本控制。但是在go module
中,最小版本選擇原理將遵循A項目聲明的版本,即v1.0.1
如果隨后當前項目又引入了模塊B的新代碼怎么辦?將模塊B導入項目后,Go會將項目的模塊D版本從v1.0.1升到v1.0.2。為模塊D的所有依賴項(模塊A和B)選擇模塊D的“最小”版本,該版本當前處于所需版本集(v1.0.1和v.1.0.2)中
最后,如果刪除剛剛為模塊B添加的代碼,會發生什么?Go會將項目鎖定到模塊D的版本v1.0.2中。降級到版本v1.0.1將是一個更大的更改,而Go知道版本v1.0.1可以正常運行并且穩定,因此版本v1.0.2仍然是“最新版本”。
為了驗證最小版本選擇原理,作者嘔心瀝血設計了一個簡單的示例。
以項目github.com/dreamerjackson/mydiv
為例,讀者可以將其看做上節中的模塊D
,其v1.0.1
與v1.0.2
版本的代碼如下,只是簡單的改變了錯誤返回的字符串。
## v1.0.1
package mydiv
import "github.com/pkg/errors"
func Div(a int,b int) (int,error){
if b==0{
return 0,errors.Errorf("new error b can't = 0")
}
return a/b,nil
}
## v1.0.2
package mydiv
import "github.com/pkg/errors"
func Div(a int,b int) (int,error){
if b==0{
return 0,errors.Errorf("new error b can't = 0")
}
return a/b,nil
}
接著模塊B
即github.com/dreamerjackson/minidiv
引用了模塊D即github.com/dreamerjackson/mydiv
v1.0.1版本
## 模塊B
package div
import (
"github.com/dreamerjackson/mydiv"
)
func Div(a int,b int) (int,error){
return mydiv.Div(a,b)
}
最后當前的項目,我們將其稱為模塊Now
直接依賴了模塊D v1.0.2,同時依賴了模塊B
當前代碼如下:
package main
import (
"fmt"
div "github.com/dreamerjackson/minidiv"
"github.com/dreamerjackson/mydiv"
)
func main(){
_,err1:= mydiv.Div(4,0)
_,err2 := div.Div(4,0)
fmt.Println(err1,err2)
}
當前的依賴關系如下:
當前模塊 --> 模塊D v1.0.2
當前模塊 --> 模塊B --> 模塊D v1.0.1
因此我們將驗證,是否和我們所料,當前項目選擇了模塊D v1.0.2 呢?
驗證方式有兩種:第一種為直接運行,查看項目采用了哪一個版本的代碼
$ go run main.go
v1.0.2 b can't = 0 v1.0.2 b can't = 0
如上所示,輸出的結果全部是我們在模塊D v1.0.2中定義的代碼!
第二種方式是使用go list
指令
~/mathlib ? go list -m all | grep mydiv
github.com/dreamerjackson/mydiv v1.0.2
我們還可以通過使用go mod mhy
z指令,查看在哪里引用了包github.com/dreamerjackson/mydiv
~/mathlib ? go mod why github.com/dreamerjackson/mydiv
# github.com/dreamerjackson/mydiv
github.com/dreamerjackson/mathlib
github.com/dreamerjackson/mydiv
我們可以使用go list -m -u all
指令查看直接和間接模塊的當前和最新版本
~/mathlib ? go list -m -u all | column -t jackson@192
go: finding github.com/dreamerjackson/minidiv latest
github.com/dreamerjackson/mathlib
github.com/dreamerjackson/minidiv v0.0.0-20200305104752-fcd15cf402bb
github.com/dreamerjackson/mydiv v1.0.2 [v1.0.3]
github.com/pkg/errors v0.9.1
如上所示,我們可以看到github.com/dreamerjackson/mydiv
的當前版本為v1.0.2
,但是最新的版本為v1.0.3
獲取直接和間接模塊可以使用go get
指令。其中有不少的參數。
下面命令以最小版本原則
更新所有的直接和間接模塊
go get -t -d -v ./...
-t
考慮構建測試所需的模塊
-d
下載每個模塊的源代碼
-v
提供詳細輸出
./…
在整個源代碼樹中執行這些操作,并且僅更新所需的依賴項
注意,除非你了解項目的所有細節,否則慎用全部的最大最新版本的更新
如果go get中使用-u
參數會用最大最新版本
原則更新所有的直接和間接模塊
~/mathlib ? go get -u -t -d -v ./... jackson@192
go: finding github.com/dreamerjackson/minidiv latest
go: downloading github.com/dreamerjackson/mydiv v1.0.3
go: extracting github.com/dreamerjackson/mydiv v1.0.3
接著我們可以再次查看當前引用的版本,我們會發現模塊github.com/dreamerjackson/mydiv
已經強制更新到了最新的v1.0.3
~/mathlib ? go list -m all | grep mydiv jackson@192
github.com/dreamerjackson/mydiv v1.0.3
如果您不滿意所選的模塊和版本,則始終可以通過刪除go.mod go.sum
模塊文件并再次運行go mod tidy來重置。當項目還不太成熟時這是一種選擇。
$ rm go.*
$ go mod init <module name>
$ go mod tidy
Go模塊引入了一種新的導入路徑語法,即語義導入版本控制。每個語義版本均采用vMAJOR.MINOR.PATCH的形式。
MAJOR 主版本號,如果有大的版本更新,導致 API 和之前版本不兼容。我們遇到的就是這個問題。
MINOR 次版本號,當你做了向下兼容的新 feature。
PATCH 修訂版本號,當你做了向下兼容的修復 bug fix。
v 所有版本號都是 v 開頭。
如果兩個版本具有相同的主編號,則預期更高版本(如果您愿意,更大版本)將與較早版本(較小版本)向后兼容。但是,如果兩個版本的主要編號不同,則它們之間沒有預期的兼容性關系。
因此我們在上面的實例中可以看到,go預料到v1.0.3與v1.0.1是兼容的,因為他們有相同的主版本號1
。但是一般我們將版本升級到了v2.0.0
,即被認為是出現了重大的更新。
如上圖實例顯示了go對于版本更新的處理。my/thing/v2
標識特定模塊的語義主版本2
。版本1是my/thing
,模塊路徑中沒有明確的版本。但是,當您引入主要版本2或更大版本時,必須在模塊名稱后添加版本,以區別于版本1和其他主要版本,因此版本2為my/thing/v2,版本3為my/thing/v3,依此類推。
假設模塊A引入了模塊B和模塊C,模塊B引入了模塊Dv1.0.0,模塊C引入了模塊Dv2.0.0。則看起來就像是
A --> 模塊B --> 模塊D v1.0.0
A --> 模塊C --> 模塊D v2.0.0
由于v1 和v2 模塊的路徑不相同,因此他們之間會是互不干擾的兩個模塊。
下面我們用實例來驗證
首先我們給mydiv打一個v2.0.0的tag,其代碼如下,簡單修改了錯誤文字v2.0.0 b can't = 0
package mydiv
import "github.com/pkg/errors"
func Div(a int,b int) (int,error){
if b==0{
return 0,errors.Errorf("v2.0.0 b can't = 0")
}
return a/b,nil
}
同時需要修改v2模塊路徑名為:
module github.com/dreamerjackson/mydiv/v2
接著在mathlib中,代碼如下:
package main
import (
"fmt"
div "github.com/dreamerjackson/minidiv"
mydiv "github.com/dreamerjackson/mydiv/v2"
)
func main(){
_,err1:= mydiv.Div(4,0)
_,err2 := div.Div(4,0)
fmt.Println(err1,err2)
}
現在的依賴路徑可以表示為為:
mathlib --> 直接引用mydiv v2
mathlib --> 直接引用minidiv --> 間接引用mydiv v1
當我們運行代碼之后,會發現兩段代碼是共存的
v2.0.0 b can't = 0 ::v1.0.1 b can't = 0
接著執行go list
,模塊共存,驗證成功~
~/mathlib(master*) ? go list -m all | grep mydiv
github.com/dreamerjackson/mydiv v1.0.1
github.com/dreamerjackson/mydiv/v2 v2.0.1
模塊鏡像于2019年八月推出,是go官方1.13版本的默認系統。模塊鏡像是一個代理服務器,以幫助加快構建本地應用程序所需的模塊的獲取。代理服務器實現了基于REST的API,并根據Go工具的需求進行了設計。
模塊鏡像將會緩存已請求的模塊及其特定版本,從而可以更快地檢索將來的請求。一旦代碼被獲取并緩存在模塊鏡像中,就可以將其快速提供給世界各地的用戶。
checksum數據庫也于2019八月推出,是可以用來防止模塊完整性、有效性的手段。它驗證特定版本的任何給定模塊代碼的正確性,而不管何人何時何地以及是如何獲取的。Google擁有唯一的校驗和數據庫,但是可以通過私有模塊鏡像對其進行緩存。
有幾個環境變量可以控制與模塊鏡像和checksum數據庫有關的行為
GOPROXY:一組指向模塊鏡像的URL,用于獲取模塊。如果您希望Go工具僅直接從VCS地址獲取模塊,則可以將其設置為direct
。如果將此設置為off
,則將不會下載模塊
GOSUMDB:用于驗證給定模塊/版本的代碼的checksum數據庫地址。此地址用于形成一個適當的URL,該URL告訴Go工具在哪里執行這些checksum校驗和查找。該URL可以指向Google擁有的checksum數據庫,也可以指向支持對checksum數據庫進行緩存或代理的本地模塊鏡像。如果您不希望Go工具驗證添加到go.sum文件中的給定模塊/版本的哈希碼,也可以將其設置為off,僅在將任何新module添加到go.sum文件之時,才查詢checksum數據庫
GONOPROXY:一組基于URL的模塊路徑,這些模塊不會使用模塊鏡像來獲取,而應直接在VCS地址上獲取。
GONOSUMDB:一組基于URL的模塊路徑,這些模塊的哈希碼不會在checksum數據庫中查找。
GOPRIVATE:一個方便變量,用于將GONOPROXY和GONOSUMDB設置為相同的默認值
我們可以通過go env
來查看到這些默認值
$ go env
GONOPROXY=""
GONOSUMDB=""
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOSUMDB="sum.golang.org"
這些默認值告訴Go工具使用Google模塊鏡像和Google checksum數據庫。如果這些Google服務可以訪問您所需的所有代碼,則建議使用此配置。如果Google模塊鏡像恰好以410(消失)或404(未找到)響應,則使用direct(這是GOPROXY配置的一部分)將允許Go工具更改路線并直接獲取模塊/版本VCS位置。
例如,如果我們需要訪問所有代理服務器,例如需要權限的位于gitlab
等地的代碼,我們可以使用export GOPRIVATE=gitlab.XXX.com,gitlab.XXX-XX.com,XXX.io
多個域名用逗號分隔。
Athens是一個私有模塊鏡像,可以用于搭建私有模塊鏡像。使用私有模塊鏡像的一個原因是允許緩存公共模塊鏡像無法訪問的私有模塊。Athens項目提供了一個在Docker Hub
上發布的Docker
容器,因此不需要特殊的安裝。
docker run -p '3000:3000' gomods/athens:latest
接下來,啟動一個新的終端會話以運行Athens,為大家演示其用法。啟動Athens服務并通過額外的參數調試日志(請確保系統已經安裝并啟動了docker)并有科學*上網的環境
$ docker run -p '3000:3000' -e ATHENS_LOG_LEVEL=debug -e GO_ENV=development gomods/athens:latest
INFO[7:11AM]: Exporter not specified. Traces won't be exported
2020-03-06 07:11:30.671249 I | Starting application at port :3000
接著我們修改GOPROXY參數,指向本地3000
端口,初始化我們之前的項目,再次執行go mod tidy
$ export GOPROXY="http://localhost:3000,direct"
$ rm go.*
$ go mod init github.com/dreamerjackson/mathlib
$ go mod tidy
在Athens日志中即可查看對應信息
INFO[7:39AM]: incoming request http-method=GET http-path=/github.com/dreamerjackson/mydiv/@v/list http-status=200
INFO[7:39AM]: incoming request http-method=GET http-path=/github.com/dreamerjackson/minidiv/@v/list http-status=200
INFO[7:39AM]: incoming request http-method=GET http-path=/github.com/dreamerjackson/minidiv/@latest http-status=200
詳細信息,查看參考資料中Athens的官方網站
提供脫離gopath
管理go代碼的優勢
提供了代碼捆綁、版本控制、依賴管理的功能
供全球開發人員使用、構建,下載,授權、驗證,獲取,緩存和重用模塊(可以通過搭建自己的代理服務器來實現這些功能)
可以驗證模塊(對于任何給定的版本)始終包含完全相同的代碼,而不管它被構建了多少次,從何處獲取以及由誰獲取
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。