從 go-chi 框架撤回所有主版本聊 Go1.16 的新特性
閱讀本文大概需要 10?分鐘。
大家好,我是站長(zhǎng) polarisxu。
在 Reddit 上看到一條消息:
go-chi is retracting all major versions with go1.16 new retract directive.
go.mod 的變更如下:

這利用了 Go 1.16 中 Module 的新特性。在這之前,先一起學(xué)習(xí)下該特性。
01 retract:撤回版本
也許不少人沒(méi)有開(kāi)發(fā)過(guò)自己的 Module(模塊),但了解模塊版本撤回還是有必要的,說(shuō)不定哪天就能用到。因此建議你能夠跟著本文操作一遍。
一般地,模塊作者需要一種方法來(lái)指示不應(yīng)該使用某個(gè)已發(fā)布的模塊版本??赡艹鲇谝韵聨c(diǎn)原因:
發(fā)現(xiàn)了一個(gè)嚴(yán)重的安全漏洞; 發(fā)現(xiàn)了嚴(yán)重的不兼容性或 bug; 這個(gè)版本是偶然發(fā)布的,或是過(guò)早發(fā)布了;
作者不能簡(jiǎn)單地刪除版本標(biāo)簽(tag),因?yàn)樗鼈兒芸赡茉谀K代理上仍然可用。如果作者能夠從所有代理中刪除一個(gè)版本,那么依賴該版本的下游用戶就會(huì)無(wú)法使用,出問(wèn)題。
此外,作者在發(fā)布了 go.sum 文件之后也不能更改版本,校驗(yàn)和數(shù)據(jù)庫(kù)會(huì)驗(yàn)證發(fā)布的版本從未更改。
那怎么辦?作者應(yīng)該能夠撤回模塊版本。撤回的模塊版本是模塊作者明確聲明不應(yīng)該使用的版本。(retract 這個(gè)詞是從學(xué)術(shù)文獻(xiàn)中借來(lái)的:一篇被撤回的研究論文仍然可用,但是它有問(wèn)題,不應(yīng)該成為未來(lái)工作的基礎(chǔ))。
撤回的版本應(yīng)該在模塊代理和原始代碼庫(kù)中保持可用。依賴已撤回版本的構(gòu)建應(yīng)可以繼續(xù)工作。當(dāng)用戶依賴于一個(gè)已撤回的版本(直接或間接)時(shí),應(yīng)該通知用戶。它也應(yīng)該很難無(wú)意中升級(jí)到一個(gè)已撤回版本。
為了讓大家更好的理解模塊撤回功能,本文通過(guò)具體的例子來(lái)演示。我們會(huì)創(chuàng)建兩個(gè)模塊:
foo,這是一個(gè)模塊,本文將其 push 到 GitHub,完整路徑:https://github.com/polaris1119/foo,你本地試驗(yàn)記得路徑使用你的。本文將發(fā)布該模塊的許多版本。 gopher,一個(gè)簡(jiǎn)單的 main 包,使用了上面的模塊,該模塊不會(huì)發(fā)布,只是作為本地模塊;
注意,請(qǐng)確保 Go 版本是 1.16+。
本文演示用的操作系統(tǒng)是 MacOS。
創(chuàng)建 foo 模塊
$?mkdir?-p?~/foo
$?cd?~/foo
$?git?init?-q
$?git?remote?add?origin?https://github.com/polaris1119/foo
$?go?mod?init?github.com/polaris1119/foo
go:?creating?new?go.mod:?module?github.com/polaris1119/foo
在模塊目錄中創(chuàng)建一個(gè)文件 foo.go,輸入如下內(nèi)容:
package?foo
func?Bar()?string?{
??return?"這是初始版本"
}
注意:在把該模塊代碼提交之前,先在 GitHub 上創(chuàng)建好項(xiàng)目 foo。
將 foo 模塊的改動(dòng)提交 git 并 push。
$?git?add?-A
$?git?commit?-q?-m?"Initial?commit"
$?git?branch?-M?main
$?git?push?-u?origin?main
這是該模塊的初始版本,我們用 v0 語(yǔ)言版本來(lái)標(biāo)記它,表明它還不穩(wěn)定。
$?git?tag?v0.1.0
$?git?push?-q?origin?v0.1.0
foo 的第一個(gè)版本已經(jīng)發(fā)布,我們看看使用它的 gopher 模塊。
創(chuàng)建 gopher 模塊
該模塊就放在本地,因此不用設(shè)置 git:
$?mkdir?~/gopher
$?cd?~/gopher
$?go?mod?init?gopher
創(chuàng)建一個(gè) main.go 文件,內(nèi)容如下:
package?main
import?(
????"fmt"
????"github.com/polaris1119/foo"
)
func?main()?{
?????fmt.Println(foo.Bar())
}
接下來(lái),我們通過(guò) go get 顯示指定依賴 foo 的版本:
$?go?get?github.com/polaris1119/[email protected]
go:?downloading?github.com/polaris1119/foo?v0.1.0
go?get:?added?github.com/polaris1119/foo?v0.1.0
注意:我本地配置了 GOPROXY=https://goproxy.cn,direct
然后運(yùn)行:
$?go?run?.
這是初始版本
目前一切正常。
一個(gè)更好的版本
經(jīng)過(guò)一段時(shí)間,foo 的功能有一些變化,這里假設(shè) Bar 方法語(yǔ)句變了(一個(gè)兼容的變化)。
回到 foo 模塊,修改代碼如下:
package?foo
func?Bar()?string?{
??return?"這是一個(gè)更好的版本"
}
提交并推送到 GitHub。
但現(xiàn)在 foo 模塊還不是穩(wěn)定版,因此不想標(biāo)記為 v1。安全起見(jiàn),我們發(fā)布 v0.2.0:
$?git?tag?v0.2.0
$?git?push?-q?origin?v0.2.0
現(xiàn)在 foo 發(fā)布了 v0.2.0 版本,我們?cè)?gopher 項(xiàng)目嘗試下。
$?cd?~/gopher
$?go?get?github.com/polaris1119/[email protected]
go:?downloading?github.com/polaris1119/foo?v0.2.0
go?get:?upgraded?github.com/polaris1119/foo?v0.1.0?=>?v0.2.0
然后 go run 運(yùn)行:
$?go?run?.
這是一個(gè)更好的版本
這時(shí),你發(fā)現(xiàn) “這是一個(gè)更好的版本” 不好,怎么辦?你可以修改掉,然后再發(fā)布一個(gè)新版本。然而,gopher 項(xiàng)目已經(jīng)依賴了 v0.2.0,怎么辦?
撤回模塊版本
我們可以在 go.mod 中增加 retract 指令來(lái)撤回某個(gè)模塊版本。
可以通過(guò)命令實(shí)現(xiàn):(也可以直接修改 go.mod 文件)
$?go?mod?edit?-retract=v0.2.0
這時(shí) go.mod 內(nèi)容如下:
module?github.com/polaris1119/foo
go?1.16
retract?v0.2.0
一般建議在 retract 上加上撤回的原因。go get、go list 等會(huì)顯示這個(gè)原因。
module?github.com/polaris1119/foo
go?1.16
//?Bar?方法返回值不友好
retract?v0.2.0
修改下 Bar 的返回內(nèi)容,提交 GitHub 并發(fā)布 v0.3.0:
func?Bar()?string?{
??return?"這是v0.3.0版本"
}
v0.3.0 發(fā)布后,回到 gopher 模塊,使用這個(gè)新版本。
$?cd?~/gopher
$?go?get?github.com/polaris1119/[email protected]
go:?downloading?github.com/polaris1119/foo?v0.3.0
go?get:?upgraded?github.com/polaris1119/foo?v0.2.0?=>?v0.3.0
這一步確保 https://goproxy.cn 這個(gè)代理知曉了 v0.3.0 版本,這是一種手動(dòng)讓代理拉取你模塊的方法。
$?go?run?.
這是v0.3.0版本
已經(jīng)正常了。
經(jīng)過(guò)這個(gè)步驟到底發(fā)生了什么?我們通過(guò)以下命令看一下:
$?go?list?-m?-versions?github.com/polaris1119/foo
github.com/polaris1119/foo?v0.1.0?v0.3.0
v0.2.0 不見(jiàn)了。通過(guò)增加 -retracted 選項(xiàng)可以查看撤回的版本:
$?go?list?-m?-versions?-retracted?github.com/polaris1119/foo
github.com/polaris1119/foo?v0.1.0?v0.2.0?v0.3.0
如果我們依賴回收的 v0.2.0 版本會(huì)怎么樣了?
$?go?get?github.com/polaris1119/[email protected]
go:?warning:?github.com/polaris1119/[email protected]:?retracted?by?module?author:?Bar?方法返回值不友好
go:?to?switch?to?the?latest?unretracted?version,?run:
?go?get?github.com/polaris1119/foo@latest?go?get:?downgraded?github.com/polaris1119/foo?v0.3.0?=>?v0.2.0
提示信息還是挺友好的,告知你 v0.2.0 是一個(gè)撤回的版本。
雖然警告,但 v0.2.0 可以正常使用嗎?
$?go?run?.
這是一個(gè)更好的版本
發(fā)現(xiàn)一切正常。
有了這個(gè)功能,有一些模塊可能會(huì)使用它。那怎么知曉我們的項(xiàng)目有沒(méi)有依賴撤回的版本呢?使用 go list 命令即可:
$?go?list?-m?-u?all
gopher
github.com/polaris1119/foo?v0.2.0?(retracted)?[v0.3.0]
我們現(xiàn)在回到最新版本:
$?go?get?github.com/polaris1119/foo@latest
go?get:?upgraded?github.com/polaris1119/foo?v0.2.0?=>?v0.3.0
為 foo 模塊增加功能
又過(guò)了一段時(shí)間,我們?yōu)?foo 模塊增加了新的功能:
func?Quz()?string?{
????return?"This?is?Quz?function"
}
提交到 GitHub,并發(fā)布 v0.4.0,依然是不穩(wěn)定版本。
$?git?tag?v1.0.0
$?git?push?-q?origin?v1.0.0
但很糟糕的是,我不小心發(fā)布了 v1.0.0,這樣會(huì)讓用戶以為你的模塊已經(jīng)是穩(wěn)定版本,但實(shí)際上并不是這樣。所以,我們想撤回 v1.0.0。
在撤回這個(gè)版本之前,我們應(yīng)該先發(fā)布 v0.4.0 版本:
$?git?tag?v0.4.0
$?git?push?-q?origin?v0.4.0
要撤回 v1.0.0,我們需要發(fā)布 v1.0.1(為什么?因?yàn)槲覀円獙?xiě)入撤回的信息)。不過(guò)這樣一來(lái),我們還得撤回 v1.0.1,死循環(huán)了。。。go module 允許我們指定一個(gè)撤回的版本范圍,這次手動(dòng)編輯 go.mod 文件。
module?github.com/polaris1119/foo
go?1.16
retract?(
????//?Bar?方法返回值不友好
????v0.2.0
????//?v1?提前發(fā)布了
????[v1.0.0,?v1.0.1]
)
提交這次改動(dòng)到 GitHub,然后再創(chuàng)建 v1.0.1 版本。
$?git?tag?v1.0.1
$?git?push?-q?origin?v1.0.1
接著切到 gopher 模塊。
為了讓 https://goproxy.cn 知曉 v1.0.0 等版本,我們先獲取它。
$?go?get?github.com/polaris1119/[email protected]
$?go?get?github.com/polaris1119/[email protected]
$?go?get?github.com/polaris1119/[email protected]
注意,切換到 v1.0.x 版本時(shí),很可能看不到版本撤回的信息,因?yàn)?proxy 可能還沒(méi)自動(dòng)定期更新。一般需要等待一段時(shí)間,比如 1 分鐘。如果沒(méi)有看到警告信息,等待 1 分鐘后再試,應(yīng)該能看到。
現(xiàn)在列出所有的版本:
$?go?list?-m?-versions?-retracted?github.com/polaris1119/foo
github.com/polaris1119/foo?v0.1.0?v0.2.0?v0.3.0?v0.4.0?v1.0.0?v1.0.1
或只列出未撤回版本:
$?go?list?-m?-versions?github.com/polaris1119/foo
github.com/polaris1119/foo?v0.1.0?v0.3.0?v0.4.0
贊!v0.4.0 是該模塊的新版本。
這時(shí)可以更新下 gopher,來(lái)使用 foo 的新功能:(加上對(duì) foo.Quz 函數(shù)的調(diào)用)
$?go?run?.
這是v0.3.0版本
This?is?Quz?function
提示:如果你將來(lái)要發(fā)布 v1 穩(wěn)定版,應(yīng)該從 v1.0.2 開(kāi)始,因?yàn)?v1.0.0 和 v1.0.1 被占用了。
02 關(guān)于 incompatible
講解完 retract 指令后,先看本文開(kāi)頭截圖中的另外一個(gè)東西:incompatible。
在 go-chi 框架中,v2.x.x、v3.x.x 和 v4.x.x 都加上了 incompatible,這是什么意思?
Go 模塊的版本號(hào)需要遵循 v 的格式,當(dāng) major 大于 1 時(shí),版本號(hào)需要體現(xiàn)在模塊名中,比如 Echo 框架:github.com/labstack/echo/v4。
然而,由于 Go 模塊功能出現(xiàn)較晚(Go1.11 才出現(xiàn)),在它出現(xiàn)之前,很多項(xiàng)目的版本號(hào)已經(jīng)大于 1 了,比如 Echo 框架,這些版本連 go.mod 文件都沒(méi)有,更別提模塊名加上版本號(hào)。于是,這些版本就會(huì)有 incompatible 標(biāo)記。
因?yàn)槟K名沒(méi)有版本信息,導(dǎo)致無(wú)法判斷版本的兼容性問(wèn)題,比如 v2.x.x 和 v3.x.x 都是 incompatible 的,使用 v2.x.x 的項(xiàng)目,更新依賴時(shí),會(huì)直接升級(jí)到 v3.x.x,這顯然是不行的,因此才標(biāo)記它們?yōu)?incompatible(不兼容)。
你可以在上面做這個(gè)試驗(yàn):新增版本 v2.0.0,但不修改 go.mod 文件中的 module 名,看看最新版本是否會(huì)帶 incompatible。
一般不建議項(xiàng)目使用 incompatible,畢竟穩(wěn)定性沒(méi)法保證,它是不符合 Go Module 規(guī)范的。
03 go-chi 撤回所有主版本
先介紹下 chi 這個(gè)框架:
lightweight, idiomatic and composable router for building Go HTTP services
它主要強(qiáng)調(diào)自己是一個(gè)路由,方便構(gòu)建 HTTP 服務(wù)。它兼容 net/http,沒(méi)有任何第三方依賴。
簡(jiǎn)單使用示例:
package?main
import?(
?"net/http"
?"github.com/go-chi/chi"
?"github.com/go-chi/chi/middleware"
)
func?main()?{
?r?:=?chi.NewRouter()
?r.Use(middleware.Logger)
?r.Get("/",?func(w?http.ResponseWriter,?r?*http.Request)?{
??w.Write([]byte("welcome"))
?})
?http.ListenAndServe(":3000",?r)
}
有興趣的自己去了解。這里主要說(shuō)下它撤回主版本的事情。
chi 保證自己有很好的兼容性,而作者特別厭煩模塊名帶版本號(hào),即 github.com/go-chi/chi/v4 這樣的(有強(qiáng)迫癥),但 chi 項(xiàng)目 tag 已經(jīng)到 4.x.x 了,怎么辦?
從上面截圖看,它一直使用的 incompatible。沒(méi)想過(guò)到 Go1.16 除了 retract 的功能,于是 chi 作者做了一個(gè)決定:在已經(jīng)發(fā)布的版本中,只保留 v1.5.x 系列,其他的全部撤回。
$?go?list?-m?-versions?github.com/go-chi/chi
github.com/go-chi/chi?v1.5.0?v1.5.1?v1.5.2?v1.5.3
沒(méi)有了一大堆帶 incompatible 的版本,世界瞬間清靜了。
不過(guò)它的這個(gè)決定,有不少人反對(duì),reddit 上也是激烈討論。
作者表示:https://github.com/go-chi/chi/issues/561
對(duì)于給您帶來(lái)的不便,我再次表示歉意,但是我為此項(xiàng)目投入了數(shù)年甚至數(shù)千小時(shí)的時(shí)間,對(duì)此我非常感興趣,SIV 是我堅(jiān)決反對(duì)的事情,因此我不會(huì)采納它。盡管許多人不同意,但請(qǐng)記住,這是 OSS,不是以任何方式贊助或付費(fèi)的,您始終可以選擇 fork 它并維護(hù)自己的版本。
作者很強(qiáng)硬。(誰(shuí)知道 SIV 是什么意思?)
實(shí)際上使用 retract,之前的版本依然可以正常使用。我個(gè)人比較支持 chi 作者的做法。你呢?歡迎交流你的看法。
歡迎關(guān)注我
