Go語言重新開始,Go Modules 的前世今生與基本使用

點擊一鍵訂閱《雲薦大咖》專欄,獲取官方推薦精品內容,學技術不迷路!

 

 

2020 年騰訊內部的一份開發者報告顯示,Go 語言已經成爲騰訊內部第二大後端開發語言,在騰訊每天有大量的 Go 開發者在做業務和平臺開發,大量的團隊和項目使用也暴露出一些問題,隨着 Go Modules 的出現,類似於內部自簽發證書、安全審計等這些問題也逐漸得到解決。

 

筆者在騰訊當前負責騰訊雲在 Go 編程語言使用上的一些問題, 2021年初開始負責內部 goproxy 服務並推廣Go Modules使用,這些技術支撐了騰訊雲、微信、騰訊視頻、騰訊遊戲、騰訊音樂、騰訊會議等明星產品,並與公司內部軟件源團隊、工蜂團隊、TRPC 團隊以及各個 CI 團隊達成密切的合作。在本系列文章中,筆者就將由淺入深幫助大家開始學習和了解 Go Modules。

 

Golang 開發的模式演進

 

從 Go 出生開始,用戶就一直在使用 GOPATH 這個環境變量,隨着 Go 語言的快速發展和不斷壯大,由 GOPATH 引起的編譯依賴問題也開始逐漸出現。終於在2019 年, Golang 迎來 10 週年之際,Google Go 團隊終於開始把目光投向了這一伴隨了 Golang 十年的環境變量。

 

GOPATH的使用

 

目前在 Go 中存在兩種開發模式,GOPATH mode 和 Go modules mode。

 

在 Go modules 之前,Go 開發中的依賴管理使用 GOPATH 開發模式。在 GOPATH 開發模式中,Go 命令使用 GOPATH 環境變量來實現以下幾個功能:

  1. go install 命令安裝二進制庫到  GOBINGOBIN,其默認路徑爲GOPATH/bin。
  2. go install 命令安裝編譯好的包到 GOPATH/pkg/example.com/y/zGOPATH/pkg/中,例如將example.com/y/z安裝到GOPATH/pkg/example.com/y/z.a。
  3. go get 命令下載源碼包到 GOPATH/src/example.com/y/zGOPATH/src/中,例如將example.com/y/z下載到GOPATH/src/example。

 

Go modules的發展歷程

 

GOPATH mode 開發模式是終將要被淘汰的,Go 官方在整個 Go 開發生態系統中添加 package version 這一概念,進而引入 Go modules 開發模式。從 GOPATH mode 開發模式變換到 Go modules 是一個漫長的過程,它已經經歷了數個 Go 的發行版本:

 

  •   Go 1.11 (2018 年 8 月) 引入了 GO111MODULE 環境變量,其默認值爲 auto。如果設置該變量 GO111MODULE=off, 那麼 go 命令將始終使用 GOPATH mode 開發模式。如果設置該變量 GO111MODULE=on,go 命令將始終使用 Go modules 開發模式。如果設置該變量 GO111MODULE=auto (或者不設置),go 命令行會根據當前工作目錄來決定使用哪種模式,如果當前目錄在 GOPATH/srcgo.modgoGomodule使GOPATHGOPATH/src以外,並且在根目錄下存在go.mod文件,那麼go命令會啓用Gomodule模式,否則使用GOPATH開發模式。這個規則保證了所有在GOPATH/src 中使用 auto 值時原有編譯不受影響,並且還可以在其他目錄中來體驗最新的  Go module 開發模式。
  •  Go 1.13 (2019 年 8 月) 調整了 GO111MODULE=auto 模式中對 GOPATH/srcGOPATH/src的限制,如果一個代碼庫在GOPATH/src 中,並且有 go.mod 文件的存在, go 命令會啓用 module 開發模式。這允許用戶繼續在基於導入的層次結構中組織他們的檢出代碼,但使用模塊進行個別倉庫的導入。
  •  Go 1.16 (2021 年 2 月) 會將 GO111MODULE=on 做爲默認值,默認啓用 go module 開發模式,也就是說,默認情況下 GOPATH 開發模式將被徹底關閉。如果用戶需要使用 GOPATH 開發模式可以指定環境變量 GO111MODULE=auto 或者 GO111MODULE=off。
  • Go 1.NN (???) 將會廢棄 GO111MODULE 環境變量和 GOPATH 開發模式,默認完全使用 module 開發模式。

 

GOPATH 與Go modules的相愛想殺

 

針對大家關心的幾個問題,筆者對此作出如下回答:

Q1:GOPATH 變量會被移除嗎?

A:不會,GOPATH 變量不會被移除。未來廢棄 GOPATH 開發模式並不是指刪除 GOPATH 環境變量,它會繼續保留,主要作用如下:

  •  go install 命令安裝二進制到 GOBINGOBIN目錄,其默認位置爲GOPATH/bin。
  •  go get 命令緩存下載的 modules 到 GOMODCACHEGOMODCACHE目錄,默認位置爲GOPATH/pkg/mod。
  • go get 命令緩存下載的 checksum 數據到 $GOPATH/pkg/sumdb 目錄。

 

Q2:我還可以繼續在 `GOPATH/src/import/path` 中創建代碼庫嗎?

A:可以,很多開發者以這樣的文件結構來組織自己的倉庫,你只需要在自己創建的倉庫中添加 go.mod 文件。

 

Q3: 如果我想測試修改一個我需要的依賴庫,我改怎麼做?

A:如果你編譯自己的項目時依賴了一些未發佈的變更,你可以使用 go.mod 的 replace來實現你的需求。

舉個例子,如果你已經將 golang.org/x/website 和 golang.org/x/tools 下載到  GOPATH/src/GOPATH/src/目錄下,那麼你可以在GOPATH/src/golang.org/x/website/go.mod 中添加下面的指令來完成替換:

replace golang.org/x/tools => $GOPATH/src/golang.org/x/tools

當然,replace 指令是不感知 GOPATH 的,將代碼下載到其他目錄也一樣可以。

 

從0開始使用 Go Modules

 

1. 創建一個新的 Go module

首先創建一個新目錄 /home/gopher/hello,然後進入到這個目錄中,接着創建一個新文件, hello.go:

 

1
2
3
4
5
6
7
8
9
package hello
 
  
 
func Hello() string {
 
    return "Hello, world."
 
}

  

然後再寫個對應的測試文件 hello_test.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
 
    }
 
}

  

現在我們擁有了一個 package,但它還不是一個 module,因爲還沒有創建 go.mod 文件。如果在 /home/gopher/hello 目錄中執行 go test,則可以看到:

1
2
3
$ go test
 
go: go.mod file not found in current directory or any parent directory; see 'go help modules'

  

可以看到 Go 命令行提示沒有找到 go.mod 文件,可以參考 go help modules。這樣的話可以使用 Go mod init 來初始化一下,然後再執行 Go test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ go mod init example.com/hello
 
go: creating new go.mod: module example.com/hello
 
go: to add module requirements and sums:
 
  
 
go mod tidy
 
$go mod tidygo
 
$ go test
 
PASS
 
ok      example.com/hello   0.020s
 
$

  

這樣的話,module 測試就完成了。

然後執行的 go mod init 命令創建了一個 go.mod 文件:

1
2
3
4
5
6
7
$ cat go.mod
 
module example.com/hello
 
  
 
go 1.17

  

2. 給 module 添加依賴

Go modules 的主要亮點在於在編程時使用別人寫的代碼,即引入一個依賴庫時能有非常好的體驗。首先更新一下 hello.go,引入 rsc.io/quote 來實現一些新的功能。

p

1
2
3
4
5
6
7
8
9
10
11
12
13
ackage hello
 
  
 
import "rsc.io/quote"
 
  
 
func Hello() string {
 
    return quote.Hello()
 
}

  

然後,再測試一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ go test
 
hello.go:3:8: no required module provides package rsc.io/quote; to add it:
 
  
 
go get rsc.io/quote
 
$ go get rsc.io/quote
 
go: downloading rsc.io/quote v1.5.2
 
go: downloading rsc.io/sampler v1.3.0
 
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
 
go: added golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
 
go: added rsc.io/quote v1.5.2
 
go: added rsc.io/sampler v1.3.0
 
$ go test
 
PASS

  

ok      example.com/hello   1.401s

從 Go 1.7開始,Go modules 開始使用 lazyloading 加載機制,依賴庫的更新需要根據提示手動進行相應的更新。

 Go 命令會根據 go.mod 的文件來解析拉取指定的依賴版本。如果在 go.mod 中沒有找到指定的版本,會提示相應的命令引導用戶添加,然後 Go 命令會去解析最新的穩定版本(Latest),並且添加到 go.mod 文件中。 在這個例子中可以看到,第一次執行的 Go test 運行需要 rsc.io/quote 這個依賴,但是在 go.mod 文件中並沒有找到,於是引導用戶去獲取 latest 版本,用戶通過 go get rsc.io/quote 獲取了最新版本 v1.5.2,而且還另外下載了另外兩個 rsc.io/quote 需要的依賴:rsc.io/sampler 和 golang.org/x/text。間接依賴引用也會記錄在 go.mod 文件中,使用 indirect 註釋進行標記。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat go.mod
 
module example.com/hello
 
  
 
go 1.17
 
  
 
require (
 
    golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
 
    rsc.io/quote v1.5.2 // indirect
 
    rsc.io/sampler v1.3.0 // indirect
 
)

  

再一次運行 go test 命令不會重複上面的工作,因爲 go.mod 已經是最新的,並且所需的依賴包已經下載到本機中了(在 $GOPATH/pkg/mod 中):

1
2
3
4
5
$ go test
 
PASS
 
ok      example.com/hello   0.020s

  

注意,雖然 Go 命令可以快速輕鬆地添加新的依賴項,但並非沒有代價。

如上面所提到,在項目中添加一個直接依賴可能會引入其他間接依賴。go list -m all 命令可以列出當前項目所依賴的所有依賴:

1
2
3
4
5
6
7
8
9
$ go list -m all
 
example.com/hello
 
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
 
rsc.io/quote v1.5.2
 
rsc.io/sampler v1.3.0

  

在 go list 的輸出結果中,可以看到當前的 module,也被稱爲 main module 會展示在第一行,然後其他的會按照 module path 進行排序。其中依賴 golang.org/x/text 的版本號 v0.0.0-20170915032832-14c0d48ead0c 是一個僞版本號, 它是 Go 版本的一種,指向了一個沒有打 tag 的 commit 上。

另外除了 go.mod 文件,go 命令還維護了一個叫做 go.sum 的文件,這個文件包含了每個版本對應的加密哈希值。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat go.sum
 
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
 
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
 
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
 
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
 
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
 
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...

  

Go 命令使用 go.sum 來保證每次下載的依賴庫代碼和第一次都是一致的,進而來保證項目不會出現一些異常情況,所以 go.mod 和 go.sum 都應該上傳到 git 等版本控制系統中。

 

3. 更新依賴

從上面 go list -m all 命令的輸出中,可以看到在庫 golang.org/x/text 中使用了一個僞版本號。首先把這個版本好更新到最新的穩定版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ go get golang.org/x/text
 
go: downloading golang.org/x/text v0.3.7
 
go: upgraded golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c => v0.3.7
 
  
 
$ go test
 
PASS
 
ok      example.com/hello   0.013s
 
測試仍然可以通過,然後再執行一次 go list -m all:
 
$ go list -m all
 
example.com/hello
 
golang.org/x/text v0.3.7
 
rsc.io/quote v1.5.2
 
rsc.io/sampler v1.3.0
 
  
 
$ cat go.mod
 
module example.com/hello
 
  
 
go 1.17
 
  
 
require (
 
    golang.org/x/text v0.3.7 // indirect
 
    rsc.io/quote v1.5.2 // indirect
 
    rsc.io/sampler v1.3.0 // indirect
 
)

  

結語

Go 通過 Go modules 的依賴管理統一了 Go 生態中衆多的第三方的依賴管理,並且高度集成在 Go 命令行中,無需開發者們額外安裝使用,目前在當前維護的 Go 版本中都已經支持了 Go modules。還沒有切換到 Go modules 的用戶,強烈建議大家開始使用它,這無論是從團隊開發體驗、性能還是安全等方面,都能提供諸多的優質特性和保障。

 

 

 《雲薦大咖》是騰訊雲加社區精品內容專欄。雲薦官特邀行業佼者,聚焦於前沿技術的落地及理論實踐之上,持續爲您解讀雲時代熱點技術、探索行業發展新機。點擊一鍵訂閱,我們將爲你定期推送精品內容。

 

 

 

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