我們一起 Go Modules知識點,你學會了嗎?
Go Modules發(fā)展史
go get階段
起初Go語言在1.5之前沒有依賴管理工具,若想引入依賴庫,需要執(zhí)行go get命令將代碼拉取放入GOPATH/src目錄下,作為GOPATH下的全局依賴,這也就意味著沒有版本控制及隔離項目的包依賴;
vendor階段
為了解決隔離項目的包依賴問題,Go1.5版本推出了vendor機制,環(huán)境變量中有一個GO15VENDOREXPERIMENT需要設置為1,該環(huán)境變量在Go1.6版本時變成默認開啟,目前已經(jīng)退出了歷史舞臺;
vendor其實就是將原來放在GOPATH/src的依賴包放到工程的vendor目錄中進行管理,不同工程獨立地管理自己的依賴包,相互之間互不影響,原來是包共享的模式,通過vendor這種機制進行隔離,在項目編譯的時候會先去vendor目錄查找依賴,如果沒有找到才會再去GOPATH目錄下查找;
優(yōu)點:保證了功能項目的完整性,減少了下載依賴包,直接使用vendor就可以編譯
缺點:仍然沒有解決版本控制問題,go get仍然是拉取最新版本代碼;
社區(qū)管理工具
很多優(yōu)秀的開發(fā)者在這期間也都實現(xiàn)了不錯的包依賴管理工具,例如:
godep:https://github.com/tools/godep
govendor:https://github.com/kardianos/govendor
glide:https://github.com/Masterminds/glide
dep:https://github.com/golang/dep
dep應該是其中最成功的,得到了Go語言官方的支持,該項目也被放到了https://github.com/golang/dep,但是為什么dep沒有稱為官宣的依賴工具呢?
其實因為隨著Russ Cox 與 Go 團隊中的其他成員不斷深入地討論,發(fā)現(xiàn) dep 的一些細節(jié)似乎越來越不適合 Go,因此官方采取了另起 proposal 的方式來推進,其方案的結果一開始先是釋出 vgo,最終演變?yōu)槲覀儸F(xiàn)在所見到的 Go modules;
go modules
go modules是Russ Cox推出來的,發(fā)布于Go1.11,成長于Go1.12,豐富于Go1.13,正式于Go1.14推薦在生產(chǎn)上使用,幾乎后續(xù)的每個版本都或多或少的有一些優(yōu)化,在Go1.16引入go mod retract、在Go1.18引入go work工作區(qū)的概念,這些我們在本文都會介紹到;
Go Modules知識點
GO111MODULE環(huán)境變量
這個環(huán)境變量是Go Modules的開關,主要有以下參數(shù):
- auto:只在項目包含了go.mod文件時啟動go modules,在Go1.13版本中是默認值
- on:無腦啟動Go Modules,推薦設置,Go1.14版本以后的默認值
- off:禁用Go Modules,一般沒有使用go modules的工程使用;
我現(xiàn)在使用的Go版本是1.19.3,默認GO111MODULE=on,感覺該變量也會像GO15VENDOREXPERIMENT最終推出系統(tǒng)環(huán)境變量的舞臺;
GOPROXY
該環(huán)境變量用于設置Go模塊代理,Go后續(xù)在拉取模塊版本時能夠脫離傳統(tǒng)的VCS方式從鏡像站點快速拉取,GOPROXY的值要以英文逗號分割,默認值是https://proxy.golang.org,direct,但是該地址在國內無法訪問,所以可以使用goproxy.cn來代替(七牛云配置),設置命令:
也可以使用其他配置,例如阿里配置:
該環(huán)境變量也可以關閉,可以設置為"off",禁止Go在后續(xù)操作中使用任何Go module proxy;
上面的配置中我們用逗號分割后面的值是direct,它是什么意思呢?
direct為特殊指示符,因為我們指定了鏡像地址,默認是從鏡像站點拉取,但是有些庫可能不存在鏡像站點中,direct可以指示Go回源到模塊版本的源地址去抓取,比如github,當go module proxy返回404、410這類錯誤時,其會自動嘗試列表中的下一個,遇見direct時回源地址抓??;
GOSUMDB
該環(huán)境變量的值是一個Go checksum database,用于保證Go在拉取模塊版本時拉取到的模塊版本數(shù)據(jù)未經(jīng)篡改,若發(fā)現(xiàn)不一致會中止,也可以將值設置為??off?
?即可以禁止Go在后續(xù)操作中校驗模塊版本;
什么是Go checksum database?
Go checksum database主要用于保護Go不會從任何拉到被篡改過的非法Go模塊版本,詳細算法機制可以看一下:https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md#proxying-a-checksum-database
GOSUMDB的默認值是sum.golang.org,默認值與自定義值的格式不一樣,默認值在國內是無法訪問,這個值我們一般不用動,因為我們一般已經(jīng)設置好了GOPROXY,goproxy.cn支持代理sum.golang.org;
GOSUMDB的值自定義格式如下:
- 格式 1:<SUMDB_NAME>+<PUBLIC_KEY>。
- 格式 2:<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>。
GONOPROXY/GONOSUMDB/GOPRIVATE
這三個環(huán)境變量放在一起說,一般在項目中不經(jīng)常使用,這三個環(huán)境變量主要用于私有模塊的拉取,在GOPROXY、GOSUMDB中無法訪問到模塊的場景中,例如拉取git上的私有倉庫;
GONOPROXY、GONOSUMDB的默認值是GOPRIVATE的值,所以我們一般直接使用GOPRIVATE即可,其值也是可以設置多個,以英文逗號進行分割;例如:
也可以使用通配符的方式進行設置,對域名設置通配符號,這樣子域名就都不經(jīng)過Go module proxy和Go checksum database;
全局緩存
go mod download會將依賴緩存到本地,緩存的目錄是GOPATH/pkg/mod/cache、GOPATH/pkg/sum,這些緩存依賴可以被多個項目使用,未來可能會遷移到$GOCACHE下面;
可以使用go clean -modcache清理所有已緩存的模塊版本數(shù)據(jù);
Go Modules命令
我們可以使用go help mod查看可以使用的命令:
命令 | 作用 |
go mod init | 生成go.mod文件 |
go mod download | 下載go.mod文件中指明的所有依賴放到全局緩存 |
go mod tidy | 整理現(xiàn)有的依賴,添加缺失或移除不使用的modules |
go mod graph | 查看現(xiàn)有的依賴結構 |
go mod edit | 編輯go.mod文件 |
go mod vendor | 導出項目所有的依賴到vendor目錄 |
go mod verify | 校驗一個模塊是否被篡改過 |
go mod why | 解釋為什么需要依賴某個模塊 |
go.mod文件
go.mod是啟用Go modules的項目所必須且最重要的文件,其描述了當前項目的元信息,每個go.mod文件開頭符合包含如下信息:
module:用于定義當前項目的模塊路徑(突破$GOPATH路徑)
go:當前項目Go版本,目前只是標識作用
require:用設置一個特定的模塊版本
exclude:用于從使用中排除一個特定的模塊版本
replace:用于將一個模塊版本替換為另外一個模塊版本,例如chromedp使用golang.org/x/image這個package一般直連是獲取不了的,但是它有一個github.com/golang/image的鏡像,所以我們要用replace來用鏡像替換它
restract:用來聲明該第三方模塊的某些發(fā)行版本不能被其他模塊使用,在Go1.16引入
例子:
接下來我們分模塊詳細介紹一下各部分;
module path
go.mod文件的第一行是module path,采用倉庫+module name的方式定義,例如上面的項目:
因為Go module遵循語義化版本規(guī)范2.0.0,所以如果工程的版本已經(jīng)大于2.0.0,按照規(guī)范需要加上major的后綴,module path改成如下:
go version
go.mod文件的第二行是go version,其是用來指定你的代碼所需要的最低版本:
其實這一行不是必須的,目前也只是標識作用,可以不寫;
require
require用來指定該項目所需要的各個依賴庫以及他們的版本,從上面的例子中我們看到版本部分有不同的寫法,還有注釋,接下來我們來解釋一下這部分;
indirect注釋
以下場景才會添加indirect注釋:
- 當前項目依賴包A,但是A依賴包B,但是A的go.mod文件中缺失B,所以在當前項目go.mod中補充B并添加indirect注釋
- 當前項目依賴包A,但是依賴包A沒有go.mod文件,所以在當前項目go.mod中補充B并添加indirect注釋
- 當前項目依賴包A,依賴包A又依賴包B,當依賴包A降級不在依賴B時,這個時候就會標記indirect注釋,可以執(zhí)行go mod tidy移除該依賴;
Go1.17版本對此做了優(yōu)化,indirect 的 module 將被放在單獨 require 塊的,這樣看起來更加清晰明了。
incompatible標記
我們在項目中會看到有一些庫后面添加了incompatible標記:
jwt-go這個庫就是這樣的,這是因為jwt-go的版本已經(jīng)大于2了,但是他們的module path仍然沒有添加v2、v3這樣的后綴,不符合Go的module管理規(guī)范,所以go module把他們標記為incompatible,不影響引用;
版本號
go module拉取依賴包本質也是go get行為,go get主要提供了以下命令:
命令 | 作用 |
go get | 拉取依賴,會進行指定性拉?。ǜ拢?,并不會更新所依賴的其它模塊。 |
go get -u | 更新現(xiàn)有的依賴,會強制更新它所依賴的其它全部模塊,不包括自身。 |
go get -u -t ./... | 更新所有直接依賴和間接依賴的模塊版本,包括單元測試中用到的。 |
go get拉取依賴包取決于依賴包是否有發(fā)布的tags:
- 拉取的依賴包沒有發(fā)布tags
- 默認取主分支最近一次的commit的commit hash,生成一個偽版本號
- 拉取的依賴包有發(fā)布tags
如果只有單個模塊,那么就取主版本號最大的那個tag
如果有多個模塊,則推算相應的模塊路徑,取主版本號最大的那個tag
沒有發(fā)布的tags:
v0.0.0:根據(jù)commit的base version生成的:
- 如果沒有base version,那么就是vx.0.0的形式
- 如果base version是一個預發(fā)版本,那么就是vx.y.z-pre.0的形式
- 如果base version是一個正式發(fā)布的版本,那么它就patch號加1,就是vx.y.(z+1)-0的形式
?20190718012654:是這次提交的時間,格式是??yyyyMMddhhmmss?
?
fb15b899a751:是這個版本的commit id,通過這個可以確定這個庫的特定的版本
replace
replace用于解決一些錯誤的依賴庫的引用或者調試依賴庫;
場景舉例:
舉例1:
日常開發(fā)離不開第三方庫,大部分場景都可以滿足我們的需要,但是有些時候我們需要對依賴庫做一些定制修改,依賴庫修改后,我們想引起最小的改動,就可以使用replace命令進行重新引用,調試也可以使用replace進行替換,Go1.18引入了工作區(qū)的概念,調試可以使用work進行代替,后面會介紹;
舉例2:
golang.org/x/crypto庫一般我們下載不下來,可以使用replace引用到github.com/golang/crypto:
exclude
用于跳過某個依賴庫的版本,使用場景一般是我們知道某個版本有bug或者不兼容,為了安全起可以使用exclude跳過此版本;
retract
這個特性是在Go1.16版本中引入,用來聲明該第三方模塊的某些發(fā)行版本不能被其他模塊使用;
使用場景:發(fā)生嚴重問題或者無意發(fā)布某些版本后,模塊的維護者可以撤回該版本,支持撤回單個或多個版本;
這種場景以前的解決辦法:
維護者刪除有問題版本的tag,重新打一個新版本的tag;
使用者發(fā)現(xiàn)有問題的版本tag丟失,手動介入升級,并且不明真因;
引入retract后,維護者可以使用retract在go.mod中添加有問題的版本:
重新發(fā)布新版本后,在引用該依賴庫的使用執(zhí)行go list可以看到 版本和"嚴重bug..."的提醒;
該特性的主要目的是將問題更直觀的反饋到開發(fā)者的手中;
go.sum文件
go.sun文件也是在go mod init階段創(chuàng)建,go.sum的介紹文檔偏少,我們一般也很少關注go.sum文件,go.sum主要是記錄了所有依賴的module的校驗信息,內容如下:
image-20230102193717816
從上面我們可以看到主要是有兩種形式:
- h1:
- /go.mod h1:
其中module是依賴的路徑,version是依賴的版本號。hash是以??h1:?
?開頭的字符串,hash 是 Go modules 將目標模塊版本的 zip 文件開包后,針對所有包內文件依次進行 hash,然后再把它們的 hash 結果按照固定格式和算法組成總的 hash 值。
h1 hash 和 go.mod hash兩者要不同時存在,要不就是只存在go.mod hash,當Go認為肯定用不到某個版本的時候就會省略它的h1 hash,就只有go.mod hash;
Go Modules在項目中使用
使用go modules的一個前置條件是Go語言版本大于等于Go1.11;
然后我們要檢查環(huán)境變量GO111MODULE是否開啟,執(zhí)行go env查看:
執(zhí)行如下命令打開go mod:
接下來我們隨意創(chuàng)建一個項目:
執(zhí)行go mod init初始化該項目:
接下來我們在demo目錄下創(chuàng)建main.go文件,寫下如下代碼:
然后執(zhí)行go mod tidy命令:
自動根據(jù)main.go文件更新依賴,我們再看一下go.mod文件:
以上就是在項目對go.mod的簡單使用;
go1.18新特性:工作區(qū)
工作區(qū)用來解決什么問題?
場景1:我們有時在本地會對一些三方依賴庫進行特制修改,然后想在項目修改依賴庫引用到本地進行調試,這時我們可以使用replace做替換,這樣就可以在本地進行開發(fā)聯(lián)調,這樣雖然可以解決問題,但是會存在問題,因為是在項目的go.mod文件直接修改的,如果誤傳到遠程倉庫,會影響到其他開發(fā)同學;
場景2:我們在本地開發(fā)了一些依賴庫,這時想在本地測試一下,還未發(fā)到遠程倉庫,那么我們在其他項目中引入該依賴庫后,執(zhí)行??go mod tidy?
?就會報遠程庫沒有找到的問題,所以就必須要把依賴庫先推送到遠程,在引用調試;
正是這些問題,Go語言在Go1.18正式增加了go work工作區(qū)的概念,其實就是將N個Go Module組成一個Go Work,工作區(qū)的讀取優(yōu)先級是最高的,執(zhí)行go help work可以查看go work提供的功能:
執(zhí)行go work init命令初始化一個新的工作區(qū),在項目中生成一個go.work文件:
go.work文件與go.mod文件語法一致,go.work支持三個指令:
- go:聲明go版本號
- use:聲明應用所依賴模塊的具體文件路徑,路徑可以是絕對路徑或相對路徑,即使路徑是當前應用目錄外也可
- replace:聲明替換某個模塊依賴的導入路徑,優(yōu)先級高于 go.mod 中的 replace 指令;
所以針對上述場景,我們使用go work init命令在項目中對本地依賴庫進行關聯(lián)即可解決,后續(xù)我們只需要在git配置文件中添加go.work文件不推送到遠程即可;
我們也可以在編譯時通過-workfile=off指令禁用工作區(qū)模式:
go.work的推出主要是用于在本地調試,不會因為修改go.mod引入問題;
參考文獻
- Go1.18 新特性:多 Module 工作區(qū)模式
- Go Modules 終極入門
- Go mod 七宗罪
- 深入Go Module之go.mod文件解析
總結
現(xiàn)在大小公司的項目應該都已經(jīng)在使用Go Modules?進行依賴包管理了,雖然Go Modules?相比于Maven、npm還不是很完善,但也在不斷地進行優(yōu)化,變得越來越好,如果你現(xiàn)在項目還沒有使用go modules,可以準備將項目遷移到go mod了,推薦你使用;
好啦,本文到這里就結束了,我是asong,我們下期見。