【翻譯】【Go】Go Modules的用法
簡介
Go 1.11 和 1.12 都對模組有了初步的支援,使得對依賴的管理更加詳細和容易。這個文章對模組的使用做一個基本的介紹。
模組是對Go包一個集合,以一個檔案樹的形式儲存在根目錄的go.mod
檔案中。go.mod
檔案定義了模組的路徑,還有相關的依賴項。每一個依賴項都會被認為是一個模組路徑和指定的版本規則,其實就是依賴項也是一個模組。
在Go 1.11的時候,Go命令列就有了對模組的支援,噹噹前的目錄或者父目錄有go.mod
檔案的時候,並且可以在GOPATH
以外的地方使用,為了相容性,即使在GOPATH
中發現了go.mod
檔案,也會使用GOPATH
的方式來載入檔案。從Go 1.13開始,模組將被預設支援。
本文將介紹使用模組開發Go程式碼時出現的一系列常見操作:
建立模組
讓我們來建立一個模組。
在$GOPATH/src
目錄之外建立一個新的,空的目錄,進入目錄然後建立一個新檔案hello.go
,
package hello
func Hello() string {
return "Hello,world"
}
複製程式碼
在hello_test.go
檔案中寫一個測試
package hello
import "testing"
func TestHello(t *testing.T) {
want := "Hello,world"
if got := Hello(); got != want {
t.Errorf("Hello() = %q,want %q" ,got,want)
}
}
複製程式碼
這是,當前目錄中包含了一個包,並不是一個模組,因為這裡還沒有go.mod
檔案,如果我們在當前目錄下執行go test
,可以看到:
D:\Code\go>go test
PASS
ok _/D_/Code/go 0.327s
D:\Code\go>
複製程式碼
最後一行總結了整體包測試。 因為我們在$GOPATH
之外以及任何模組之外工作,所以go命令不知道當前目錄的匯入路徑,並根據目錄名稱構成假路徑:_/D_/Code/go
。
現在,我們在當前目錄下執行go mod init
,然後再進行go test
:
D:\Code\go>go mod init example.com/hello
go: creating new go.mod: module example.com/hello
D:\Code\go>go test
PASS
ok example.com/hello 0.393s
D:\Code\go>
複製程式碼
恭喜!你已經寫了一個模組,並且通過了測試
go mod init
命令生成一個go.mod
檔案:
PS D:\Code\go> cat .\go.mod module example.com/hello
go 1.12
PS D:\Code\go>
複製程式碼
go.mod
檔案只會出現在模組的根目錄,子目錄中的包具有匯入路徑,包括模組路徑和子目錄的路徑。例如,我們建立一個子目錄world
,我們可以不在再次執行go mod init
,包會自動載入並且可以被識別出是example.com/hello
的一部分,匯入路徑就是example.com/hello/world
。
新增依賴
Go模組的主要作用是提升可以使用其他開發人員提供的程式碼。
更新hello.go
,匯入rsc.io/quote
包:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
複製程式碼
再一次執行go test
D:\Code\go>go test
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
--- FAIL: TestHello (0.00s)
hello_test.go:10: Hello() = "Hello,world.",want "Hello,world"
FAIL
exit status 1
FAIL example.com/hello 0.431s
D:\Code\go>
複製程式碼
go命令會解析go.mod
檔案中指定的依賴和版本。當遇見一個在go.mod
中沒有提供的包時,go命令會自動查詢這個包並且新增到go.mod
檔案中,使用最新的版本(最新的版本指的時被打標籤的穩定版,或者最新的預釋出版本,或者最新的版本),在我們這個例子中,go test
解析了一個新的包rsc.io/quote
,版本為v1.5.2。還下載了兩個包rsc.io/quote
使用的兩個依賴項,就是rsc.io/sampler
和golang.org/x/test
。go.mod
檔案指記錄直接的依賴關係:
D:\Code\go>type go.mod
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2
D:\Code\go>
複製程式碼
再一次執行go test
的時候,就不會重複上一次的任務了,因為go.mod
現在已經更新了,並且依賴已經被快取到了本地($GOPATH/pkg/mod)
正如我們看到的,新增一個依賴經常會帶來其他的依賴,go list -m
命令可以列出當前模組的所有依賴
yankeweideMacBook-Pro:hello yankewei$ go list -m all
hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
yankeweideMacBook-Pro:hello yankewei$
複製程式碼
在輸出的內容中,當前的模組名總是會出現在第一行,緊跟著按依賴的模組路徑排序。
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
是一個偽版本號,這個go系統對於一個沒有打標的commit的標識。
除了go.mod之外,go命令還維護一個名為go.sum的檔案,其中包含特定模組版本內容的預期加密雜湊:
yankeweideMacBook-Pro:hello yankewei$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
yankeweideMacBook-Pro:hello yankewei$
複製程式碼
go使用go.sum檔案來確保未來下載這些模組的時候保證和第一次下載的一樣,也確保你專案的依賴不會改變,所以go.mod和go.sum都應該納入版本控制中。
更新依賴
在Go模組中,版本使用語義化的標記來表示的。一個語義化的版本有三個部分:主版本,次要版本,修補版本。例如:對於 v0.1.2,主版本號是0,次要版本是1,修補版本是2。
執行go list -m all
會看到golang.org/x/text
是沒有標籤的版本。現在來把它更新到最新版本,並且測試一下看是否可以正常工作:
$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok example.com/hello 0.013s
$
複製程式碼
很好!測試通過,再執行go list -m all
,並且看一下go.mod檔案的內容:
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote v1.5.2
)
$
複製程式碼
golang.org/x/test
包已經被更新到了最新的版本(v0.3.0)。indirect
註釋表示依賴沒有被這個模組直接使用,僅僅被其他的模組間接使用,可以通過go help modules
檢視相信資訊。
現在讓我們嘗試更新rsc.io/sampler
版本。
$ go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99
$ go test
--- FAIL: TestHello (0.00s)
hello_test.go:8: Hello() = "99 bottles of beer on the wall,99 bottles of beer,...",world."
FAIL
exit status 1
FAIL example.com/hello 0.014s
$
複製程式碼
Shit!測試失敗了,最新的版本和我們要使用的不相容,先看一下這個模組的可用版本:
$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
$
複製程式碼
我們已經使用過v1.3.0;v1.99.99版本不相容,可以嘗試使用v1.3.1:
$ go get rsc.io/[email protected]
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok example.com/hello 0.022s
$
複製程式碼
我們在go get
的引數中指定@v1.3.1
。預設是@latest
,也就是最新的版本
新增一個新的主版本
我們新增一個新的函式func Proverb
,函式會呼叫rsc.io/quote/v3
的模組,在這個模組中可以呼叫quote.Concurrency
,返回一個字串,首次我們先來更新hello.go檔案:
package hello
import (
"rsc.io/quote"
quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
return quote.Hello()
}
func Proverb() string {
return quoteV3.Concurrency()
}
複製程式碼
然後在hello_test.go新增一個測試:
func TestProverb(t *testing.T) {
want := "Concurrency is not parallelism."
if got := Proverb(); got != want {
t.Errorf("Proverb() = %q,want)
}
}
複製程式碼
現在可以執行程式碼:
$ go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok example.com/hello 0.024s
$
複製程式碼
現在我們的專案有兩個依賴rsc.io/quote
和rsc.io/quote/v3
每個不同的主版本(v1,v2等等)的模組使用的是不同的路徑,這樣就可以選擇性的使用,並且在版本遷移的時候也可以逐步的進行。
更新依賴為新的主版本
現在我們想要把rsc.io/quote
更新到rsc.io/quote/v3
。因為主版本號已經變了,我們可能會意識到某些api被移除,被重新命名或者呼叫方法發生了改變,所以首先我們需要先看一下檔案:
$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote/v3"
Package quote collects pithy sayings.
func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string**
複製程式碼
可以看到原來的 Hello 改為了 HelloV3,那我們就可以把使用的 Hello 改為 HelloV3,並且檔案中沒有對舊版本的依賴了,可以把匯入重新命名去掉:
package hello
import (
"rsc.io/quote/v3"
)
func Hello() string {
return quote.HelloV3()
}
func Proverb() string {
return quote.Concurrency()
}
複製程式碼
可以自行測試,就不演示了。
刪除未使用的依賴
在Go語言中刪除未使用的依賴相當的簡單,只需要執行go mod tidy
。