1. 程式人生 > >初窺Go module

初窺Go module

目錄

一. 建立試驗環境

二. 傳統Go構建以及包依賴管理的回顧

三. go modules定義、experiment開關以及“依賴管理”的工作模式

四. go modules的依賴版本選擇

五. go modules與vendor

六. 小結


自2007年“三巨頭(Robert GriesemerRob PikeKen Thompson)”提出設計和實現Go語言以來,Go語言已經發展和演化了十餘年了。這十餘年來,Go取得了巨大的成就,先後在2009年和2016年當選TIOBE年度最佳程式語言,並在全世界範圍內擁有數量龐大的擁躉。不過和其他主流程式語言一樣,Go語言也不是完美的,不能滿足所有開發者的“口味”。這些年來Go在“包依賴管理

”和“缺少泛型”兩個方面飽受詬病,它們也是Go粉們最希望Go核心Team重點完善的兩個方面。

今年(2018)年初,Go核心Team的技術leader,也是Go Team最早期成員之一的Russ Cox個人部落格上連續發表了七篇文章,系統闡述了Go team解決“包依賴管理”的技術方案: vgo。vgo的主要思路包括:Semantic Import VersioningMinimal Version Selection引入Go module等。這七篇文章的釋出引發了Go社群激烈地爭論,尤其是MVS(最小版本選擇)與目前主流的依賴版本選擇方法的相悖讓很多傳統Go包管理工具的維護者“不滿”,尤其是“準官方工具”:

dep。vgo方案的提出也意味著dep專案的生命週期即將進入尾聲。

5月份,Russ Cox的Proposal “cmd/go: add package version support to Go toolchain”被accepted,這週五早些時候Russ Cox將vgo的程式碼merge到Go主幹,並將這套機制正式命名為“go module”。由於vgo專案本身就是一個實驗原型,merge到主幹後,vgo這個術語以及vgo專案的使命也就此結束了。後續Go modules機制將直接在Go主幹上繼續演化。

Go modules是go team在解決包依賴管理方面的一次勇敢嘗試,無論如何,對Go語言來說都是一個好事。在本篇文章中,我們就一起來看看這個新引入的go modules機制。

 

一. 建立試驗環境


由於加入go modules experiment機制的Go 1.11版本尚未正式釋出,且go 1.11 beta1版本釋出在go modules merge到主幹之前,因此我們要進行go module試驗只能使用Go tip版本,即主幹上的最新版本。我們需要通過編譯Go原始碼包的方式獲得支援go module的go編譯器:

編譯Go專案原始碼的前提是你已經安裝了一個釋出版,比如Go 1.10.3。然後按照下面步驟執行即可:

$ git clone https://github.com/golang/go.git
$ mv go go-tip
$ cd go-tip
$ ./all.bash
Building Go cmd/dist using /root/.bin/go1.10.2.
Building Go toolchain1 using /root/.bin/go1.10.2.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for linux/amd64.
##### Testing packages.
ok      archive/tar    0.026s
... ...
##### API check

ALL TESTS PASSED
---
Installed Go for linux/amd64 in /root/.bin/go-tip
Installed commands in /root/.bin/go-tip/bin
*** You need to add /root/.bin/go-tip/bin to your PATH.

驗證原始碼編譯方式的安裝結果:

# ./go version
go version devel +a241922 Fri Jul 13 00:03:31 2018 +0000 linux/amd64

檢視有關go module的手冊:

$  ./go help mod
usage: go mod [-v] [maintenance flags]

Mod performs module maintenance operations as specified by the
following flags, which may be combined.

The -v flag enables additional output about operations performed.

The first group of operations provide low-level editing operations
for manipulating go.mod from the command line or in scripts or
other tools. They read only go.mod itself; they do not look up any
information about the modules involved.

The -init flag initializes and writes a new go.mod to the current directory,
in effect creating a new module rooted at the current directory.
The file go.mod must not already exist.
If possible, mod will guess the module path from import comments
(see 'go help importpath') or from version control configuration.
To override this guess, use the -module flag.
(Without -init, mod applies to the current module.)

The -module flag changes (or, with -init, sets) the module's path
(the go.mod file's module line).
... ...

無法通過編譯原始碼的方式獲取go tip版的小夥伴們也不用著急,在後續即將釋出的 go 1.11 beta2 版本中將會包含對go modules的支援,到時候按常規方式安裝beta2即可體驗go modules。

 

二. 傳統Go構建以及包依賴管理的回顧


Go在構建設計方面深受Google內部開發實踐的影響,比如go get的設計就深受Google內部單一程式碼倉庫(single monorepo)和基於主幹(trunk/mainline based)的開發模型的影響:只獲取Trunk/mainline程式碼和版本無感知。

img{512x368}

Google內部基於主幹的開發模型:
– 所有開發人員基於主幹trunk/mainline開發:提交到trunk或從trunk獲取最新的程式碼(同步到本地workspace)
– 版本釋出時,建立Release branch,release branch實質上就是某一個時刻主幹程式碼的快照;
– 必須同步到release branch上的bug fix和增強改進程式碼也通常是先在主幹上提交(commit),然後再cherry-pick到release branch上

我們知道go get獲取的程式碼會放在$GOPATH/src下面,而go build會在$GOROOT/src$GOPATH/src下面按照import path去搜索package,由於go get 獲取的都是各個package repo的trunk/mainline的程式碼,因此,Go 1.5 之前的Go compiler都是基於目標Go程式依賴包的trunk/mainline程式碼去編譯的。這樣的機制帶來的問題是顯而易見的,至少包括:

  • 因依賴包的trunk的變化,導致不同人獲取和編譯你的包/程式時得到的結果實質是不同的,即不能實現reproduceable build
  • 因依賴包的trunk的變化,引入不相容的實現,導致你的包/程式無法通過編譯
  • 因依賴包演進而無法通過編譯,導致你的包/程式無法通過編譯

為了實現reproduceable build,Go 1.5 引入了Vendor機制,Go編譯器會優先在vendor下搜尋依賴的第三方包,這樣如果開發者將特定版本的依賴包存放在vendor下面並提交到code repo,那麼所有人理論上都會得到同樣的編譯結果,從而實現reproduceable build。

在Go 1.5釋出後的若干年,gopher們把注意力都集中在如何利用vendor解決包依賴問題,從手工新增依賴到vendor、手工更新依賴,到一眾包依賴管理工具的誕生:比如: govendorglide以及號稱準官方工具的dep,努力地嘗試著按照當今主流思路解決著諸如:“鑽石型依賴”等難題。

正當gopher認為dep將“順理成章”地升級為go toolchain一部分的時候,vgo橫空出世,並通過對“Semantic Import Versioning”和”Minimal Version Selected”的設定,在原Go tools上簡單快速地實現了Go原生的包依賴管理方案 。vgo就是go module的前身。

 

三. go modules定義、experiment開關以及“依賴管理”的工作模式


通常我們會在一個repo(倉庫)中建立一組Go package,repo的路徑比如:github.com/bigwhite/gocmpp會作為go package的匯入路徑(import path),Go 1.11給這樣的一組在同一repo下面的packages賦予了一個新的抽象概念: module,並啟用一個新的檔案go.mod記錄module的元資訊。

不過一個repo對應一個module這種說法其實並不精確也不正確,一個repo當然可以擁有多個module,很多公司或組織是喜歡用monorepo的,這樣勢必有在單一的monorepo建立多個module的需求,顯然go modules也是支援這種情況的。

img{512x368}
圖:single repo,single module

img{512x368}
圖:single monorepo,multiple modules

是時候上程式碼了!

我們在~/test下建立hello目錄(注意:$GOPATH=~/go,顯然hello目錄並不在GOPATH下面)。hello.go的程式碼如下:

// hello.go
package main

import "bitbucket.org/bigwhite/c"

func main() {
    c.CallC()
}

我們構建一下hello.go這個原始碼檔案:

# go build hello.go
hello.go:3:8: cannot find package "bitbucket.org/bigwhite/c" in any of:
    /root/.bin/go-tip/src/bitbucket.org/bigwhite/c (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/c (from $GOPATH)

構建錯誤!錯誤原因很明瞭:在本地的GOPATH下並沒有找到bitbucket.org/bigwhite/c路徑的package c。傳統fix這個問題的方法是手工將package c通過go get下載到本地(並且go get會自動下載package c所依賴的package d):

# go get bitbucket.org/bigwhite/c
# go run hello.go
call C: master branch
   --> call D:
    call D: master branch
   --> call D end

這種我們最熟悉的Go compiler從$GOPATH下(以及vendor目錄下)搜尋目標程式的依賴包的模式稱為:“GOPATH mode”

GOPATH是Go最初設計的產物,在Go語言快速發展的今天,人們日益發現GOPATH似乎不那麼重要了,尤其是在引入vendor以及諸多包管理工具後。並且GOPATH的設定還會讓Go語言新手感到些許困惑,提高了入門的門檻。Go core team也一直在尋求“去GOPATH”的方案,當然這一過程是循序漸進的。Go 1.8版本中,如果開發者沒有顯式設定GOPATH,Go會賦予GOPATH一個預設值(在linux上為$HOME/go)。雖說不用再設定GOPATH,但GOPATH還是事實存在的,它在go toolchain中依舊發揮著至關重要的作用。

Go module的引入在Go 1.8版本上更進了一步,它引入了一種新的依賴管理mode:“module-aware mode”。在該mode下,某原始碼樹(通常是一個repo)的頂層目錄下會放置一個go.mod檔案,每個go.mod檔案定義了一個module,而放置go.mod檔案的目錄被稱為module root目錄(通常對應一個repo的root目錄,但不是必須的)。module root目錄以及其子目錄下的所有Go package均歸屬於該module,除了那些自身包含go.mod檔案的子目錄。

在“module-aware mode”下,go編譯器將不再在GOPATH下面以及vendor下面搜尋目標程式依賴的第三方Go packages。我們來看一下在“module-aware mode”下hello.go的構建過程:

我們首先在~/test/hello下建立go.mod:

// go.mod
module hello

然後構建hello.go

# go build hello.go
go: finding bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02
go: finding bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
go: downloading bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
go: downloading bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02

# ./hello
call C: master branch
   --> call D:
    call D: master branch
   --> call D end

我們看到go compiler並沒有去使用之前已經下載到GOPATH下的bitbucket.org/bigwhite/c和bitbucket.org/bigwhite/d,而是主動下載了這兩個包併成功編譯。我們看看執行go build後go.mod檔案的內容:

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
    bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02 // indirect
)

我們看到go compiler分析出了hello module的依賴,將其放入go.mod的require區域。由於c、d兩個package均沒有版本釋出(打tag),因此go compiler使用了c、d的當前最新版,並以Pseudo-versions的形式記錄之。並且我們看到:hello module並沒有直接依賴d package,因此在d的記錄後面通過註釋形式標記了indirect,即非直接依賴,也就是傳遞依賴。

在“module-aware mode”下,go compiler將下載的依賴包快取在$GOPATH/src/mod下面:

// $GOPATH/src/mod
# tree -L 3
.
├── bitbucket.org
│   └── bigwhite
│       ├── [email protected]
│       └── [email protected]
├── cache
│   ├── download
│   │   ├── bitbucket.org
│   │   ├── golang.org
│   │   └── rsc.io
│   └── vcs
│       ├── 064503657de46d4574a6ab937a7a3b88fee03aec15729f7493a3dc8e35cc6d80
│       ├── 064503657de46d4574a6ab937a7a3b88fee03aec15729f7493a3dc8e35cc6d80.info
│       ├── 0c8659d2f971b567bc9bd6644073413a1534735b75ea8a6f1d4ee4121f78fa5b
... ...

我們看到c、d兩個package也是按照“版本”進行快取的,便於後續在“module-aware mode”下進行包構建使用。

Go modules機制在go 1.11中是experiment feature,按照Go的慣例,在新的experiment feature首次加入時,都會有一個特性開關,go modules也不例外,GO111MODULE這個臨時的環境變數就是go module特性的experiment開關。GO111MODULE有三個值:auto、on和off,預設值為auto。GO111MODULE的值會直接影響Go compiler的“依賴管理”模式的選擇(是GOPATH mode還是module-aware mode),我們詳細來看一下:

  • 當GO111MODULE的值為off時,go modules experiment feature關閉,go compiler顯然會始終使用GOPATH mode,即無論要構建的原始碼目錄是否在GOPATH路徑下,go compiler都會在傳統的GOPATH和vendor目錄(僅支援在gopath目錄下的package)下搜尋目標程式依賴的go package;

  • 當GO111MODULE的值為on時(export GO111MODULE=on),go modules experiment feature始終開啟,與off相反,go compiler會始終使用module-aware mode,即無論要構建的原始碼目錄是否在GOPATH路徑下,go compiler都不會在傳統的GOPATH和vendor目錄下搜尋目標程式依賴的go package,而是在go mod命令的快取目錄($GOPATH/src/mod)下搜尋對應版本的依賴package;

  • 當GO111MODULE的值為auto時(不顯式設定即為auto),也就是我們在上面的例子中所展現的那樣:使用GOPATH mode還是module-aware mode,取決於要構建的原始碼目錄所在位置以及是否包含go.mod檔案。如果要構建的原始碼目錄不在以GOPATH/src為根的目錄體系下,且包含go.mod檔案(兩個條件缺一不可),那麼使用module-aware mode;否則使用傳統的GOPATH mode。

 

四. go modules的依賴版本選擇


1. build list和main module

go.mod檔案一旦建立後,它的內容將會被go toolchain全面掌控。go toolchain會在各類命令執行時,比如go get、go build、go mod等修改和維護go.mod檔案。

之前的例子中,hello module依賴的c、d(indirect)兩個包均沒有顯式的版本資訊(比如: v1.x.x),因此go mod使用Pseudo-versions機制來生成和記錄c, d的“版本”,我們可以通過下面命令檢視到這些資訊:

# go list -m -json all
{
    "Path": "hello",
    "Main": true,
    "Dir": "/root/test/hello"
}
{
    "Path": "bitbucket.org/bigwhite/c",
    "Version": "v0.0.0-20180714063616-861b08fcd24b",
    "Time": "2018-07-14T06:36:16Z",
    "Dir": "/root/go/src/mod/bitbucket.org/bigwhite/[email protected]"
}
{
    "Path": "bitbucket.org/bigwhite/d",
    "Version": "v0.0.0-20180714005150-3e3f9af80a02",
    "Time": "2018-07-14T00:51:50Z",
    "Indirect": true,
    "Dir": "/root/go/src/mod/bitbucket.org/bigwhite/[email protected]"
}

go list -m輸出的資訊被稱為build list,也就是構建當前module所要構建的所有相關package(及版本)的列表。在輸出資訊中我們看到 “Main”: true這一資訊,標識當前的module為“main module”。所謂main module,即是go build命令執行時所在當前目錄所歸屬的那個module,go命令會在當前目錄、當前目錄的父目錄、父目錄的父目錄…等下面尋找go.mod檔案,所找到的第一個go.mod檔案對應的module即為main module。如果沒有找到go.mod,go命令會提示下面錯誤資訊:

# go build test/hello/hello.go
go: cannot find main module root; see 'go help modules'

當然我們也可以使用下面命令簡略輸出build list:

# go list -m all
hello
bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02

2. module requirement

現在我們給c、d兩個package打上版本資訊:

package c:
v1.0.0
v1.1.0
v1.2.0

package d:
v1.0.0
v1.1.0
v1.2.0
v1.3.0

然後清除掉$GOPATH/src/mod目錄,並將hello.mod重新置為初始狀態(只包含module欄位)。接下來,我們再來構建一次hello.go:

// ~/test/hello目錄下

# go build hello.go
go: finding bitbucket.org/bigwhite/c v1.2.0
go: downloading bitbucket.org/bigwhite/c v1.2.0
go: finding bitbucket.org/bigwhite/d v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.3.0

# ./hello
call C: v1.2.0
   --> call D:
    call D: v1.3.0
   --> call D end

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.2.0 // indirect (c package被標記為indirect,這似乎是當前版本的一個bug)
    bitbucket.org/bigwhite/d v1.3.0 // indirect
)

我們看到,再一次初始構建hello module時,Go compiler不再用最新的commit revision所對應的Pseudo-version,而是使用了c、d兩個package的最新發布版(c:v1.2.0,d: v1.3.0)。

如果我們對使用的c、d版本有特殊約束,比如:我們使用package c的v1.0.0,package d的v1.1.0版本,我們可以通過go mod -require來操作go.mod檔案,更新go.mod檔案中的require段的資訊:

# go mod -require=bitbucket.org/bigwhite/[email protected]
# go mod -require=bitbucket.org/bigwhite/[email protected]

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.0.0 // indirect
    bitbucket.org/bigwhite/d v1.1.0 // indirect
)

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.1.0
go: finding bitbucket.org/bigwhite/c v1.0.0
go: downloading bitbucket.org/bigwhite/c v1.0.0
go: downloading bitbucket.org/bigwhite/d v1.1.0

# ./hello
call C: v1.0.0
   --> call D:
    call D: v1.1.0
   --> call D end

我們看到由於我們顯式地修改了對package c、d兩個包的版本依賴約束,go build構建時會去下載package c的v1.0.0和package d的v1.1.0版本並完成構建。

3. module query

除了通過傳入[email protected]給go mod -requirement來精確“指示”module依賴之外,go mod還支援query表示式,比如:

# go mod -require='bitbucket.org/bigwhite/[email protected]>=v1.1.0'

go mod會對query表示式做求值,得出build list使用的package c的版本:

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.1.0
    bitbucket.org/bigwhite/d v1.1.0 // indirect
)

# go build hello.go
go: downloading bitbucket.org/bigwhite/c v1.1.0
# ./hello
call C: v1.1.0
   --> call D:
    call D: v1.1.0
   --> call D end

go mod對module query進行求值的演算法是“選擇最接近於比較目標的版本(tagged version)”。以上面例子為例:

query text: >=v1.1.0
比較的目標版本為v1.1.0
比較形式:>=

因此,滿足這一query的最接近於比較目標的版本(tagged version)就是v1.1.0。

如果我們給package d增加一個約束“小於v1.3.0”,我們再來看看go mod的選擇:

# go mod -require='bitbucket.org/bigwhite/[email protected]<v1.3.0'
# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.1.0 // indirect
    bitbucket.org/bigwhite/d <v1.3.0
)

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.2.0
go: downloading bitbucket.org/bigwhite/d v1.2.0

# ./hello
call C: v1.1.0
   --> call D:
    call D: v1.2.0
   --> call D end

我們看到go mod選擇了package d的v1.2.0版本,根據module query的求值演算法,v1.2.0恰是最接近於“小於v1.3.0”的tagged version。

用下面這幅示意圖來呈現這一演算法更為直觀一些:

img{512x368}

4. minimal version selection(mvs)

到目前為止,我們所使用的example都是最最簡單的,hello module所依賴的package c和package d並沒有自己的go.mod,也沒有定義自己的requirements。對於複雜的包依賴場景,Russ Cox在“Minimal Version Selection”一文中給過形象的演算法解釋(注意:這個演算法僅是便於人類理解,但是效能低下,真正的實現並非按照這個演算法實現):

img{512x368}
例子情景

img{512x368}
演算法的形象解釋

MVS以build list為中心,從一個空的build list集合開始,先加入main module(A1),然後遞迴計算main module的build list,我們看到在這個過程中,先得到C 1.2的build list,然後是B 1.2的build list,去重合並後形成A1的rough build list,選擇集合中每個module的最新version,最終形成A1的build list。

我們改造一下我們的例子,讓它變得複雜些!

首先,我們為package c新增go.mod檔案,併為其打一個新版本:v1.3.0:

//bitbucket.org/bigwhite/c/go.mod
module bitbucket.org/bigwhite/c

require (
        bitbucket.org/bigwhite/d v1.2.0
)

在module bitbucket.org/bigwhite/c的module檔案中,我們為其新增一個requirment: bitbucket.org/bigwhite/[email protected]

接下來,我們將hello module重置為初始狀態,並刪除$GOPATH/src/mod目錄。我們修改一下hello module的hello.go如下:

package main

import "bitbucket.org/bigwhite/c"
import "bitbucket.org/bigwhite/d"

func main() {
    c.CallC()
    d.CallD()
}

我們讓hello module也直接呼叫package d,並且我們在初始情況下,給hello module新增一個requirement:

module hello

require (
    bitbucket.org/bigwhite/d v1.3.0
)

好了,這次我們再來構建一下hello module:

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.3.0
go: finding bitbucket.org/bigwhite/c v1.3.0
go: downloading bitbucket.org/bigwhite/c v1.3.0
go: finding bitbucket.org/bigwhite/d v1.2.0
# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.3.0 // indirect
    bitbucket.org/bigwhite/d v1.3.0 // indirect
)

# ./hello
call C: v1.3.0
   --> call D:
    call D: v1.3.0
   --> call D end
call D: v1.3.0

我們看到經過mvs演算法後,go compiler最終選擇了d v1.3.0版本。這裡也模仿Russ Cox的圖解給出hello module的mvs解析示意圖(不過我這個例子還是比較simple):

img{512x368}

5. 使用package d的v2版本

按照語義化版本規範,當出現不相容性的變化時,需要升級版本中的major值,而go modules允許在import path中出現v2這樣的帶有major版本號的路徑,表示所用的package為v2版本下的實現。我們甚至可以同時使用一個package的v0/v1和v2兩個版本的實現。我們依舊使用上面的例子來實操一下如何在hello module中使用package d的兩個版本的程式碼。

我們首先需要為package d建立module檔案:go.mod,並標識出當前的module為:bitbucket.org/bigwhite/d/v2(為了保持與v0/v1各自獨立演進,可通過branch的方式來實現),然後基於該版本打v2.0.0 tag。

// bitbucket.org/bigwhite/d
#cat go.mod
module bitbucket.org/bigwhite/d/v2

改造一下hello module,import d的v2版本:

// hello.go
package main

import "bitbucket.org/bigwhite/c"
import "bitbucket.org/bigwhite/d/v2"

func main() {
    c.CallC()
    d.CallD()
}

清理hello module的go.mod,僅保留對package c的requirement:

module hello

require (
    bitbucket.org/bigwhite/c v1.3.0
)

清理$GOPATH/src/mod目錄,然後重新構建hello module:

# go build hello.go
go: finding bitbucket.org/bigwhite/c v1.3.0
go: finding bitbucket.org/bigwhite/d v1.2.0
go: downloading bitbucket.org/bigwhite/c v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.2.0
go: finding bitbucket.org/bigwhite/d/v2 v2.0.0
go: downloading bitbucket.org/bigwhite/d/v2 v2.0.0

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.3.0 // indirect
    bitbucket.org/bigwhite/d/v2 v2.0.0 // indirect
)

# ./hello
call C: v1.3.0
   --> call D:
    call D: v1.2.0
   --> call D end
call D: v2.0.0

我們看到c package依然使用的是d的v1.2.0版本,而main中使用的package d已經是v2.0.0版本了。

 

五. go modules與vendor


在最初的設計中,Russ Cox是想徹底廢除掉vendor的,但在社群的反饋下,vendor得以保留,這也是為了相容Go 1.11之前的版本。

Go modules支援通過下面命令將某個module的所有依賴儲存一份copy到root module dir的vendor下:

# go mod -vendor
# ls
go.mod    go.sum  hello.go  vendor/
# cd vendor
# ls
bitbucket.org/    modules.txt
# cat modules.txt
# bitbucket.org/bigwhite/c v1.3.0
bitbucket.org/bigwhite/c
# bitbucket.org/bigwhite/d v1.2.0
bitbucket.org/bigwhite/d
# bitbucket.org/bigwhite/d/v2 v2.0.0
bitbucket.org/bigwhite/d/v2

# tree .
.
├── bitbucket.org
│   └── bigwhite
│       ├── c
│       │   ├── c.go
│       │   ├── go.mod
│       │   └── README.md
│       └── d
│           ├── d.go
│           ├── README.md
│           └── v2
│               ├── d.go
│               ├── go.mod
│               └── README.md
└── modules.txt

5 directories, 9 files

這樣即便在go modules的module-aware mode模式下,我們依然可以只用vendor下的package來構建hello module。比如:我們先刪除掉$GOPATH/src/mod目錄,然後執行:

# go build -getmode=vendor hello.go
# ./hello
call C: v1.3.0
   --> call D:
    call D: v1.2.0
   --> call D end
call D: v2.0.0

當然生成的vendor目錄還可以相容go 1.11之前的go compiler。不過由於go 1.11之前的go compiler不支援在GOPATH之外使用vendor機制,因此我們需要將hello目錄copy到$GOPATH/src下面,再用go 1.10.2版本的compiler編譯它:

# go version
go version go1.10.2 linux/amd64
~/test/hello# go build hello.go
hello.go:3:8: cannot find package "bitbucket.org/bigwhite/c" in any of:
    /root/.bin/go1.10.2/src/bitbucket.org/bigwhite/c (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/c (from $GOPATH)
hello.go:4:8: cannot find package "bitbucket.org/bigwhite/d/v2" in any of:
    /root/.bin/go1.10.2/src/bitbucket.org/bigwhite/d/v2 (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/d/v2 (from $GOPATH)

# cp -r hello ~/go/src
# cd ~/go/src/hello
# go build hello.go
# ./hello
call C: v1.3.0
   --> call D:
    call D: v1.2.0
   --> call D end
call D: v2.0.0

編譯輸出和程式的執行結果均符合預期。

 

六. 小結


go modules剛剛merge到go trunk中,問題還會有很多。merge後很多gopher也提出了諸多問題,可以在這裡查到。當然哪位朋友如果也遇到了go modules的問題,也可以在go官方issue上提出來,幫助go team儘快更好地完善go 1.11的go modules機制。

go module的加入應該算是go 1.11版本最大的變化,go module的內容很多,短時間內我的理解也可能存在偏差和錯誤,歡迎廣大gopher們交流指正。

參考資料: