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