遷移到GoModule(Migrating to Go Modules譯文)

遷移到GoModule

英文原版:https://blog.golang.org/migrating-to-go-modules

介紹

這是系列文章的第二部分

Go項目使用多種依賴管理策略,像dep和glide這種vendor模式的工具很流行,但是他們的行爲有很大差異,而且有時並不能同時正常工作。有些項目將整個GOPATH存到git倉庫,也有的只依靠go get將最新依賴安裝到GOPATH。

在Go1.11引入的module模式,提供了官方的依賴管理解決方案並內置到go命令中。本文介紹將項目遷移到module模式的工具與技術。

請注意:如果你的項目已經打上了v2.0.0或者更高的版本,則需要在添加go.mod文件時將導包路徑升級。我們將在後續文章(GoModule: v2及新版本)中解釋如何在不破壞你的用戶使用的情況下完成工作。

將項目遷移到GoModule

一個項目在準備遷移到GoModule時通常處於如下三種情況:

  • 全新的Go項目
  • 已存在的Go項目且使用了非module的依賴管理
  • 已存在的Go項目且未使用任何依賴管理

第一種情況已經在前文(使用GoModule)中介紹,因此本文主要介紹後兩種情況。

已使用依賴管理

將一個已經使用了依賴管理工具的項目遷移需要執行如下命令:

$ git clone https://github.com/my/project
[...]
$ cd project
$ cat Godeps/Godeps.json
{
    "ImportPath": "github.com/my/project",
    "GoVersion": "go1.12",
    "GodepVersion": "v80",
    "Deps": [
        {
            "ImportPath": "rsc.io/binaryregexp",
            "Comment": "v0.2.0-1-g545cabd",
            "Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
        },
        {
            "ImportPath": "rsc.io/binaryregexp/syntax",
            "Comment": "v0.2.0-1-g545cabd",
            "Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
        }
    ]
}
$ go mod init github.com/my/project
go: creating new go.mod: module github.com/my/project
go: copying requirements from Godeps/Godeps.json
$ cat go.mod
module github.com/my/project

go 1.12

require rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$

go mod init新建go.mod文件,並自動從Godeps.json``Gopkg.lock或者其他所支持的格式中導入依賴。go mod init命令的參數是module的導包路徑。

已支持的格式:

"GLOCKFILE":          ParseGLOCKFILE,
"Godeps/Godeps.json": ParseGodepsJSON,
"Gopkg.lock":         ParseGopkgLock,
"dependencies.tsv":   ParseDependenciesTSV,
"glide.lock":         ParseGlideLock,
"vendor.conf":        ParseVendorConf,
"vendor.yml":         ParseVendorYML,
"vendor/manifest":    ParseVendorManifest,
"vendor/vendor.json": ParseVendorJSON,

在下一步之前最好先執行以下go buildgo test,因爲後續步驟將會修改go.mod文件內容,因此現在的go.mod最接近之前的依賴管理工具所指定的依賴聲明。

$ go mod tidy
go: downloading rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
go: extracting rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$ cat go.sum
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca h1:FKXXXJ6G2bFoVe7hX3kEX6Izxw5ZKRH57DFBJmHCbkU=
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
$

go mod tidy命令會find所有當前module依賴或傳遞依賴的package,它會添加被已知的module依賴的package,同時刪除未被任何module使用的package。如果module提供的package僅僅被當前module所依賴的module依賴,則會被標記上// indirect。在每次commitgo.mod到版本控制之前最好都執行一下go mod tidy

然後來確保構建與測試通過:

$ go build ./...
$ go test ./...
[...]
$

注意其他依賴管理可能會在單個package或整個代碼庫(非module)級別指定依賴,並且通常並未意識到依賴項的go.mod中指定的要求。因此可能無法獲得與以前每個軟件包完全相同的版本,而且升級過去的修改可能會有風險。所以在上面的命令之後最好檢查一下依賴:

$ go list -m all
go: finding rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
github.com/my/project
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$

然後跟之前的依賴管理文件對比一下所列出的依賴是否合適。如果發現哪個版本有問題,可以執行go mod why -mgo mod graph來查看原因,然後可以用go get升級或降級到正確版本。(如果需要的版本早於所選版本,go get會根據需要降級其他依賴項以保持兼容性。)例如:

$ go mod why -m rsc.io/binaryregexp
[...]
$ go mod graph | grep rsc.io/binaryregexp
[...]
$ go get rsc.io/[email protected]
$

未使用依賴管理

對於沒有使用依賴管理的go項目,從新建go.mod文件開始:

$ git clone https://go.googlesource.com/blog
[...]
$ cd blog
$ go mod init golang.org/x/blog
go: creating new go.mod: module golang.org/x/blog
$ cat go.mod
module golang.org/x/blog

go 1.12
$

在沒有依賴管理配置文件的情況下,go mod init新建一個只包含modulegogo.mod。在本例中將module的路徑設置爲golang.org/x/blog,因爲這是他的導包路徑。用戶可以用這個路徑來import,同時必須小心注意不要修改它。

module聲明module路徑,go聲明用於編譯此module的go版本。

接下來運行go mod tidy來添加依賴:

$ go mod tidy
go: finding golang.org/x/website latest
go: finding gopkg.in/tomb.v2 latest
go: finding golang.org/x/net latest
go: finding golang.org/x/tools latest
go: downloading github.com/gorilla/context v1.1.1
go: downloading golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: extracting github.com/gorilla/context v1.1.1
go: extracting golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: downloading gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
go: extracting golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
$ cat go.mod
module golang.org/x/blog

go 1.12

require (
    github.com/gorilla/context v1.1.1
    golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
    golang.org/x/text v0.3.2
    golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
    golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
    gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
)
$ cat go.sum
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
[...]
$

go mod tidy按module中import添加所需module,並生成go.sum保存每個依賴在指定版本下的checksums。接下來確保正常工作:

$ go build ./...
$ go test ./...
ok  	golang.org/x/blog	0.335s
?   	golang.org/x/blog/content/appengine	[no test files]
ok  	golang.org/x/blog/content/cover	0.040s
?   	golang.org/x/blog/content/h2push/server	[no test files]
?   	golang.org/x/blog/content/survey2016	[no test files]
?   	golang.org/x/blog/content/survey2017	[no test files]
?   	golang.org/x/blog/support/racy	[no test files]
$

注意,當go mod tidy添加依賴是,他會添加該module的最新版本。如果你的GOPATH存在一個低版本並且它做過重大更改,那麼在執行go mod tidy或者go build``go test之後可能會報錯。如果發生了這種情況,嘗試使用go get來降級到舊版本,或者找時間使你的module兼容到依賴的最新版本。

在module模式下測試

遷移到GoModule之後有些測試需要調整。

如果測試需要在package目錄下寫文件,那麼當這個包處於只讀的module緩存時可能會失敗,進而導致go test all失敗。測試應將需要寫入的文件複製到一個臨時目錄中。

如果測試依賴相對路徑(../package-in-another-module)來查找和讀取另一個包中的文件,如果這個包在另一個module中,那麼操作會失敗。因爲該包可能存在於一個版本化的子目錄中或者是在replace指令中指定的路徑。這種情況下,應該需要把測試輸入複製到你的module,或者將測試輸入從資源文件編進go文件。

如果測試期望測試中的go命令運行在GOPATH模式將會fail。這種情況下需要在被測的文件樹中添加一個go.mod或者設置環境變量GO111MODULE=off

release

最終你需要給module打上標籤並release。如果你之前沒有發不過去任何版本,那麼這個是可選的。但是如果沒有官方release,下游的用戶就不能通過pseudo-versions來指定依賴版本,那樣更難支持。

$ git tag v1.2.0
$ git push origin v1.2.0

go.mod定義了規範的導包路徑,並添加了最低版本要求。如果你的用戶已經在用正確的導包路徑,並且你的依賴沒有大的變更,那麼添加的go.mod是向後兼容的,但是他是一個重大變更,且有可能暴露現有問題。如果已經存在了版本tag,那應該升級minor版本號。發佈GoModule這篇文章會告訴你如果升級併發布版本。

導入與規範module路徑

每個module都在go.mod中聲明瞭module路徑。module中每個引用module內部報的import聲明都必須將module路徑作爲包路徑的前綴。但是,go命令可能會通過許多不同的remote import paths遇到包含module的庫。例如,golang.org/x/lintgithub.com/golang/lint都會解析到go.googlesource.com/lint這個代碼庫。go.mod包含的這個庫聲明其路徑爲golang.org/x/lint,所以只有這個路徑對應到正確的module。

Go1.4提供了一個通過使用// import comments聲明規範的導包路徑的機制,但是package的作者並不總是提供它。結果導致在module之前編寫的代碼可能已經爲module使用了不規範的導包路徑,而沒有出現不匹配的錯誤。使用module時,導包路徑必須與規範的module路徑匹配,因此開發者可能需要更新import語句:例如,需要將github.com/golang/lint改爲golang.org/x/lint

另一中不規範的情況可能發生在使用了major版本爲2或更高的module,它的導包路徑可能與其代碼庫的路徑不同。major版本大於1的module必須在其module路徑中加上major版本號作爲後綴:例如v2.0.0版本的module必須有v2後綴。然而import語句可能已經引用了module中沒有該後綴的package,例如非module模式的用戶使用v2.0.1版本的github.com/russross/blackfriday/v2可能被寫成了github.com/russross/blackfriday,需要更新爲帶v2的情況。

結論

對於大多數用戶而言,轉換到GoModule應該是一個簡單的過程。偶然出現的問題可能是由於不規範的導包路徑或者依賴庫中有破壞性更改。後續文章將會介紹發佈v2及更高的新版本,以及異常情況調試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章