Go Module 工程化實踐(一):基礎概念篇

人的一切痛苦,本質上都是對本身的無能的憤怒。 -- 王小波

1. 基礎概念篇

Go Module已經來了,默認Go Module模式將會在1.13版本發佈。也就是說半年後,就會全面鋪開。鑑於官方提供掃盲文檔中的樣例過於簡單,提供一個更加貼近實際開發過程的例子也許是有必要的。git

官方文檔參考:Go Module Wikigithub

1.1 準備環境

按照官方的說明,Go Module是在 Go 的 1.11版本開始引入,可是默認該選項是關閉的,直到1.13版本將會默認開啓。預計1.13將會在2019年8月份發佈。因此在這以前,是必須手動開啓Go Module支持。golang

必要條件:算法

  • Go語言版本 >= 1.11
  • 設置環境變量 GO111MODULE=on

在開啓Go Module功能後,官方還提供了環境變量GOPROXY用於設置包鏡像服務。此處暫不詳細介紹了。緩存

1.2 Go Module帶來的改變

1.2.1 GOPATH做用的改變

引入Go Module後,環境變量GOPATH仍是存在的。開啓Go Module功能開關後,環境變量GOPATH的做用也發生了改變。函數

When using modules, GOPATH is no longer used for resolving imports. However, it is still used to store downloaded source code (in GOPATH/pkg/mod) and compiled commands (in GOPATH/bin).

翻譯出來就是:工具

  • 環境變量GOPATH再也不用於解析imports包路徑,即原有的GOPATH/src/下的包,經過import是找不到了。
  • Go Module功能開啓後,下載的包將存放與$GOPATH/pkg/mod路徑
  • $GOPATH/bin路徑的功能依舊保持。

1.2.2 新增go.mod文件配置

開始Go Module開發以前,首先是初始化一個正確的Go Module定義,即go.mod文件。
何爲正確的Go Module定義。就是說mod包必須符合。學習

方法一測試

  • $GOPATH/src的目錄下,建立合理的路徑github.com/liujianping/foo路徑。
  • 進入$GOPATH/src/github.com/liujianping/foo路徑,執行go mod init便可。

或者ui

方法二

  • 建立foo路徑,位置任意
  • 進入foo目錄,執行go mod init github.com/liujianping/foo便可。

生成了go.mod文件後,就該文件的語法簡單的學習一下。

  • module
    to define the module path;
  • go
    to set the expected language version;
  • require
    to require a particular module at a given version or later;
  • exclude
    to exclude a particular module version from use; and
  • replace
    to replace a module version with a different module version.

官方提供了一個簡單全面的例子:

module my/thing
go 1.12
require other/thing v1.0.2
require new/thing/v2 v2.3.4
exclude old/thing v1.2.3
replace bad/thing v1.4.5 => good/thing v1.4.5

1.2.3 go get流程改變

引入Go Module以後,go get官方又從新實現了一套。具體實現代碼能夠參考:

  • 不開啓Go Module功能,go get代碼實現
$GOROOT/src/cmd/go/internal/get/get.go
  • 開啓Go Module功能,go get代碼實現
$GOROOT/src/cmd/go/internal/modget/get.go

簡單說明一下主要區別,更詳細的go get取包原理放到下篇講解。最直接的區別是:

  • 老的go get取包過程相似:git clone + go install , 開啓Go Module功能後go get就只有 git clone 或者 download過程了。
  • 新老實現還有一個不一樣是,二者存包的位置不一樣。前者,存放在$GOPATH/src目錄下;後者,存放在$GOPATH/pkg/mod目錄下。
  • 老的go get取完主包後,會對其repo下的submodule進行循環拉取。新的go get再也不支持submodule子模塊拉取。

1.3 一個完整的例子

官方的版本因爲過於簡單,連一個基礎的本地第三方包的引入都沒有,僅僅經過引入一個公開的第三方開源包,缺乏了常規本地開發說明。因此,筆者特地提供一個完整的例子,分別從:

  • 本地倉庫
  • 遠程倉庫
  • 私有倉庫

三個維度闡釋Go Module的在實際開發中的具體應用。

本例子的目錄結構以下:

$GOPATH/src
├── github.com
    └── liujianping
        ├── demo
        │   └── go.mod
        └── foo
            └── go.mod

建立兩個mod模塊:demofoo, 其中 foo 做爲一個依賴包,提供簡單的 Greet 函數供 demo 項目調用。

1.3.1 本地倉庫

本地倉庫的意思,就是例子中的兩個包: github.com/liujianping/demogithub.com/liujianping/foo 暫時僅僅存在於本地。沒法經過 go get 直接從github.com 上獲取。

經過如下命令,簡單的建立項目代碼:

$: mkdir -p $GOPATH/src/github.com/liujianping/foo
$: cd $GOPATH/src/github.com/liujianping/foo
$: go mod init
$: cat <<EOF > foo.go
package foo

import "fmt"

func Greet(name string) string {
    return fmt.Sprintf("%s, 你好!", name)
}
EOF

$: mkdir -p $GOPATH/src/github.com/liujianping/demo
$: cd $GOPATH/src/github.com/liujianping/demo
$: go mod init
$: cat <<EOF > main.go
package  main

import ( 
    "fmt"
    "github.com/liujianping/foo"
)

func main(){
    fmt.Println(foo.Greet("GoModule"))    
}
EOF

執行完以上命令之後,mod demofoo 的代碼部分就完成了。如今來執行如下:

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
build github.com/liujianping/demo: cannot find module for path github.com/liujianping/foo

從輸出能夠看出,在demo 中調用 foo的依賴包,在編譯過程就失敗了。demo沒法找到
github.com/liujianping/foo。爲何這樣?

按照傳統的$GOPATH引入包原則,只要在$GOPATH/src存在相應路徑的包,就能夠完成編譯了。從如今的情形就能夠解釋$GOPATHGo Module功能開啓後,對原有引入包的規則發生的改變。

既然,$GOPATH/src路徑再也不支持。那麼如何解決這個沒法找到包依賴的問題呢?方法有二:

  • 本地路徑
  • 遠程倉庫

該小節提供本地路徑方法。

$: cat $GOPATH/src/github.com/liujianping/demo/go.mod
module github.com/liujianping/demo

目前demo項目的go.mod僅僅一句話,由於沒法找github.com/liujianping/foo,因此在go build過程當中也不會修改go.mod,增長對包github.com/liujianping/foo的依賴關係。因此,只能是手動處理了。修改go.mod文件以下:

module github.com/liujianping/demo

require github.com/liujianping/foo v0.0.0
replace github.com/liujianping/foo => ../foo

再次執行demo程序:

$: go run main.go
go: finding github.com/liujianping/foo v0.0.0
GoModule, 你好!

對於項目中直接引用本地依賴包的官方文檔中有段注意事項:

Note: for direct dependencies, a require directive is needed even when doing a replace. For example, if foo is a direct dependency, you cannot do replace foo => ../foo without a corresponding require for foo. (If you are not sure what version to use in the require directive, you can often use v0.0.0 such as require foo v0.0.0; see #26241).

意思就是,即便是本地依賴包,明確的require仍然是須要的。至於版本號,其實只要符合SemVer規範就能夠。能夠是v0.0.0,也能夠是v0.1.2

Go Module最主要是引入了依賴包的版本控制。因此,咱們不妨就本地版本測試一下。

對本地版本foo進行相應的git本地版本控制,增長几個版本,代碼中相應的增長版本信息。

package foo

import "fmt"

func Greet(name string) string {
    return fmt.Sprintf("%s, 你好! Version 1.0.0", name)
}

增長了如下三個版本tag。

$: git tag
v0.1.0
v0.2.0
v1.0.0

demo項目中,設置foo版本, go.mod修改以下:

module github.com/liujianping/demo

require github.com/liujianping/foo v0.1.0
replace github.com/liujianping/foo => ../foo

執行demo程序,輸出以下:

go run main.go
go: finding github.com/liujianping/foo v0.1.0
GoModule, 你好! Version 1.0.0

不可貴出結論:go get是不會從本地倉庫獲取版本信息的,查看go get在module模式下工具鏈實現代碼也可得出這個結論。

1.3.2 遠程倉庫

從上節能夠大體瞭解Go Module的原理。如今咱們將foo依賴包上傳到github.com上,包括相應的版本tag。首先github.com建立相應的項目foo.再將本地倉庫上傳到遠程倉庫中。

$: git remote add origin git@github.com:liujianping/foo.git
$: git push -u origin master

上傳版本tag信息:

$: git push origin --tags

如今完成了github.com/liujianping/foo依賴包的遠程部署。看看具體實操demo項目,首先去掉本地的直接依賴。demo項目的go.mod以下

$: cat $GOPATH/src/github.com/liujianping/demo/go.mod
module github.com/liujianping/demo

從新執行demo項目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
go: finding github.com/liujianping/foo v1.0.0
go: downloading github.com/liujianping/foo v1.0.0
GoModule, 你好! Version 1.0.0

查看變動後的go.mod,以下

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.0 // indirect

同時demo根目錄下,增長了go.sum文件。

cat go.sum
github.com/liujianping/foo v1.0.0 h1:yYoUzvOwC1g+4mXgSEloF187GmEpjKAHEmkApDwvOVQ=
github.com/liujianping/foo v1.0.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=

修改foo版本號到 v0.2.0

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v0.2.0 // indirect

從新執行demo項目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
go: finding github.com/liujianping/foo v0.2.0
go: downloading github.com/liujianping/foo v0.2.0
GoModule, 你好! Version 0.2.0

再看看go.sum文件發生的變化:

cat go.sum
github.com/liujianping/foo v0.2.0 h1:2JCV7mfUyneSksnWokX0kZoBbtWPoyL8s8iW80WHl/A=
github.com/liujianping/foo v0.2.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=
github.com/liujianping/foo v1.0.0 h1:yYoUzvOwC1g+4mXgSEloF187GmEpjKAHEmkApDwvOVQ=
github.com/liujianping/foo v1.0.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=

經過以上步驟,粗略能夠了解針對Go Module對於遠程倉庫的版本選擇。簡單解釋版本的選擇過程下:

  • 檢查遠程倉庫最新的tag版本,有就取得該版本
  • 遠程倉庫沒有tag版本時,直接獲取master分支的HEAD版本
  • 若是在go.mod文件中指定了具體版本,go get直接獲取該指定版本
  • go.mod中除了能夠指定具體版本號之外,還支持分支名

繼續對遠程版本foo增長新的版本v1.0.1。提交相應代碼並推送版本標籤v1.0.1到遠端。並從新設置demo項目中的go.mod中的依賴版本爲v1.0.0.以下:

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.0 // indirect

從新執行demo項目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
GoModule, 你好! Version 1.0.0

此次執行沒有輸出go自己的提示信息,而是直接輸出告終果。由於github.com/liujianping/foo v1.0.0已經存在於本地的緩存中了,不妨查看一下。

$: ls $GOPATH/pkg/mod/github.com/liujianping/foo@v1.0.0

雖然就demo項目而言,依賴項目foo有兩個v1.0.0v1.0.1兩個版本可用。按照GoModule版本選擇最小版本的算法,demo項目依舊選擇v1.0.0版本。

如何更新依賴包版本

更新依賴包的版本,最簡單的方式,直接手動編輯go.mod設置依賴包版本便可。

另一種方式就是經過go get -u的方式進行自動更新。具體操做步驟以下:

查看依賴包版本更新信息

$: go list -u -m all
go: finding github.com/liujianping/foo v1.0.1
github.com/liujianping/demo
github.com/liujianping/foo v1.0.0 [v1.0.1]

更新依賴包版本

$: go get -u 
go: downloading github.com/liujianping/foo v1.0.1

或者,制定更新patch版本

$: go get -u=patch github.com/liujianping/foo 
go: downloading github.com/liujianping/foo v1.0.1

此時,go.mod文件即被更新

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.1

從新執行程序

$: go run main.go
GoModule, 你好! Version 1.0.1

基於分支

GoModule除了支持基於標籤tag的版本控制,能夠直接利用遠程分支名稱進行開發。

因此本節,筆者就模塊foo建立一個新的遠程分支develop.具體代碼,請直接參考github.com/liujianping/foo項目。

修改demo項目的go.mod文件:

module github.com/liujianping/demo

require github.com/liujianping/foo develop

再次執行demo, 結果以下:

$: go run main.go
go: finding github.com/liujianping/foo develop
go: downloading github.com/liujianping/foo v1.0.2-0.20190214080857-9c0018d55446
GoModule, 你好! Branch develop

查看,go.mod文件,發生以下變動:

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.2-0.20190214080857-9c0018d55446

按官方文檔的說明,使用分支名,能夠直接拉取該分支的最後一次提交。從實驗來看, Go Module一旦發生編譯就會針對分支名的依賴進行版本號固定。

1.3.3 私有倉庫

對於私有倉庫而言,其原理與1.3.2中的遠程倉庫是相似的。惟一不一樣之處是,go get取包的過程可能存在種種障礙,致使沒法經過go get取到私有倉庫包。主要緣由多是:

  • 權限問題
  • 路徑問題

致使按照正常的go get過程取包失敗。若是瞭解了go get取包原理,以上問題也就迎刃而解了。

更多文章可直接訪問我的BLOG:GitDiG.com

相關閱讀:

相關文章
相關標籤/搜索