Skip to content

模塊

每一個現代語言都會有屬於自己的一個成熟的依賴管理工具,例如 Java 的 Gradle,Python 的 Pip,NodeJs 的 Npm 等,一個好的依賴管理工具可以為開發者省去不少時間並且可以提升開發效率。然而 Go 在早期並沒有一個成熟的依賴管理解決方案,那時所有的代碼都存放在 GOPATH 目錄下,對於工程項目而言十分的不友好,版本混亂,依賴難以管理,為了解決這個問題,各大社區開發者百家爭鳴,局面一時間混亂了起來,期間也不乏出現了一些佼佼者例如 Vendor,直到 Go1.11 官方終於推出了 Go Mod 這款官方的依賴管理工具,結束了先前的混亂局面,並在後續的更新中不斷完善,淘汰掉了曾經老舊的工具。時至今日,在撰寫本文時,Go 發行版本已經到了 1.20,在今天幾乎所有的 Go 項目都在采用 Go Mod,所以在本文也只會介紹 Go Mod,官方對於 Go 模塊也編寫了非常細致的文檔:Go Modules Reference

編寫模塊

Go Module 本質上是基於 VCS(版本控制系統),當你在下載依賴時,實際上執行的是 VCS 命令,比如git,所以如果你想要分享你編寫的庫,只需要做到以下三點:

  • 源代碼倉庫可公開訪問,且 VCS 屬於以下的其中之一
    • git
    • hg (Mercurial)
    • bzr (Bazaar)
    • svn
    • fossil
  • 是一個符合規范的 go mod 項目
  • 符合語義化版本規范

所以你只需要正常使用 VCS 開發,並為你的特定版本打上符合標准的 Tag,其它人就可以通過模塊名來下載你所編寫的庫,下面將通過示例來演示進行模塊開發的幾個步驟。

示例倉庫:246859/hello: say hello (github.com)

准備

在開始之前確保你的版本足以完全支持 go mod(go >= 1.17),並且啟用了 Go Module,通過如下命令來查看是否開啟

bash
$ go env GO111MODULE

如果未開啟,通過如下命令開啟用 Go Module

bash
$ go env -w GO111MODULE=on

創建

首先你需要一個可公網訪問的源代碼倉庫,這個有很多選擇,我比較推薦 Github。在上面創建一個新項目,將其取名為 hello,倉庫名雖然沒有什麼特別限制,但建議還是不要使用特殊字符,因為這會影響到模塊名。

創建完成後,可以看到倉庫的 URL 是https://github.com/246859/hello,對應的 go 模塊名就是github.com/246859/hello

然後將其克隆到本地,通過go mod init命令初始化模塊。

bash
$ git clone git@github.com:246859/hello.git
Cloning into 'hello'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (5/5), done.

$ cd hello && go mod init github.com/246859/hello
go: creating new go.mod: module github.com/246859/hello

編寫

然後就可以進行開發工作了,它的功能非常簡單,只有一個函數

go
// hello.go
package hello

import "fmt"

// Hello returns hello message
func Hello(name string) string {
        if name == "" {
                name = "world"
        }
        return fmt.Sprintf("hello %s!", name)
}

順便寫一個測試文件進行單元測試

go
// hello_test.go
package hello_test

import (
        "testing"
        "fmt"
        "github.com/246859/hello"
)

func TestHello(t *testing.T) {
        data := "jack"
        expected := fmt.Sprintf("hello %s!", data)
        result := hello.Hello(data)

        if result != expected {
                t.Fatalf("expected result %s, but got %s", expected, result)
        }

}

接下來繼續編寫一個命令行程序用於輸出 hello,它的功能同樣非常簡單。對於命令行程序而言,按照規范是在項目cmd/app_name/中進行創建,所以 hello 命令行程序的文件存放在cmd/hello/目錄下,然後在其中編寫相關代碼。

go
// cmd/hello/main.go
package main

import (
  "flag"
  "github.com/246859/hello"
  "os"
)

var name string

func init() {
  flag.StringVar(&name, "name", "world", "name to say hello")
}

func main() {
  flag.Parse()
  msg := hello.Hello(name)
  _, err := os.Stdout.WriteString(msg)
  if err != nil {
    os.Stderr.WriteString(err.Error())
  }
}

測試

編寫完後對源代碼格式化並測試

bash
$ go fmt && go vet ./...

$ go test -v .
=== RUN   TestHello
--- PASS: TestHello (0.00s)
PASS
ok      github.com/246859/hello 0.023s

運行命令行程序

bash
$ go run ./cmd/hello -name jack
hello jack!

文檔

最後的最後,需要為這個庫編寫簡潔明了的README,讓其它開發者看一眼就知道怎麼使用

markdown
# hello

just say hello

## Install

import code

```bash
go get github.com/246859/hello@latest
```

install cmd

```bash
go install github.com/246859/hello/cmd/hello@latest
```

## Example

Here's a simple example as follows:

```go
package main

import (
  "fmt"
  "github.com/246859/hello"
)

func main() {
  result := hello.Hello("jack")
  fmt.Println(result)
}
```

這是一個很簡單的 README 文檔,你也可以自己進行豐富。

上傳

當一切代碼都編寫並測試完畢過後,就可以將修改提交並推送到遠程倉庫。

bash
$ git add go.mod hello.go hello_test.go cmd/ example/ README.md

$ git commit -m "chore(mod): mod init" go.mod
[main 5087fa2] chore(mod): mod init
 1 file changed, 3 insertions(+)
 create mode 100644 go.mod

$ git commit -m "feat(hello): complete Hello func" hello.go
[main 099a8bf] feat(hello): complete Hello func
 1 file changed, 11 insertions(+)
 create mode 100644 hello.go

$ git commit -m "test(hello): complete hello testcase" hello_test.go
[main 76e8c1e] test(hello): complete hello testcase
 1 file changed, 17 insertions(+)
 create mode 100644 hello_test.go

$ git commit -m "feat(hello): complete hello cmd" cmd/hello/
[main a62a605] feat(hello): complete hello cmd
 1 file changed, 22 insertions(+)
 create mode 100644 cmd/hello/main.go

$ git commit -m "docs(example): add hello example" example/
[main 5c51ce4] docs(example): add hello example
 1 file changed, 11 insertions(+)
 create mode 100644 example/main.go

$ git commit -m "docs(README): update README" README.md
[main e6fbc62] docs(README): update README
 1 file changed, 27 insertions(+), 1 deletion(-)

總共六個提交並不多,提交完畢後為最新提交創建一個 tag

bash
$ git tag v1.0.0

$ git tag -l
v1.0.0

$ git log --oneline
e6fbc62 (HEAD -> main, tag: v1.0.0, origin/main, origin/HEAD) docs(README): update README
5c51ce4 docs(example): add hello example
a62a605 feat(hello): complete hello cmd
76e8c1e test(hello): complete hello testcase
099a8bf feat(hello): complete Hello func
5087fa2 chore(mod): mod init
1f422d1 Initial commit

最後再推送到遠程倉庫

bash
$ git push --tags
Enumerating objects: 23, done.
Counting objects: 100% (23/23), done.
Delta compression using up to 16 threads
Compressing objects: 100% (17/17), done.
Writing objects: 100% (21/21), 2.43 KiB | 1.22 MiB/s, done.
Total 21 (delta 5), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (5/5), done.
To github.com:246859/hello.git
   1f422d1..e6fbc62    main -> main
  * [new tag]         v1.0.0 -> v1.0.0

推送完畢後,再為其創建一個 release(有一個 tag 就足矣,release 只是符合 github 規范)

如此一來,模塊的編寫就完成了,以上就是模塊開發的一個基本流程,其它開發者便可以通過模塊名來引入代碼或安裝命令行工具。

引用

通過go get引用庫

bash
$ go get github.com/246859/hello@latest
go: downloading github.com/246859/hello v1.0.0
go: added github.com/246859/hello v1.0.0

通過go intall安裝命令行程序

bash
$ go install github.com/246859/hello/cmd/hello@latest && hello -name jack
hello jack!

或者使用go run直接運行

bash
$ go run -mod=mod github.com/246859/hello/cmd/hello -name jack
hello jack!

當一個庫被引用過後,Go Package便會為其創建一個頁面,這個過程是自動完成的,不需要開發者做什麼工作,比如 hello 庫就有一個專屬的文檔頁面,如下圖所示。

關於上傳模塊的更多詳細信息,前往Add a package

關於如何刪除模塊的信息,前往Removing a package

設置代理

Go 雖然沒有像 Maven Repo,PyPi,NPM 這樣類似的中央倉庫,但是有一個官方的代理倉庫:Go modules services (golang.org),它會根據版本及模塊名緩存開發者下載過的模塊。不過由於其服務器部署在國外,訪問速度對於國內的用戶不甚友好,所以我們需要修改默認的模塊代理地址,目前國內做的比較好的有以下幾家:

這裡選擇七牛雲的代理,執行如下命令來修改 Go 代理,其中的direct表示代理下載失敗後繞過代理緩存直接訪問源代碼倉庫。

sh
$ go env -w GOPROXY=https://goproxy.cn,direct

代理修改成功後,日後下載依賴就會非常的迅速。

下載依賴

修改完代理後,接下來安裝一個第三方依賴試試,Go 官方有專門的依賴查詢網站:Go Packages

代碼引用

在裡面搜索著名的 Web 框架Gin

這裡會出現很多搜索結果,在使用第三方依賴時,需要結合引用次數和更新時間來決定是否采用該依賴,這裡直接選擇第一個

進入對應的頁面後,可以看出這是該依賴的一個文檔頁面,有著非常多關於它的詳細信息,後續查閱文檔時也可以來這裡。

這裡只需要將它的地址復制下來,然後在之前創建的項目下使用go get命令,命令如下

sh
$ go get github.com/gin-gonic/gin

過程中會下載很多的依賴,只要沒有報錯就說明下載成功。

sh
$ go get github.com/gin-gonic/gin
go: added github.com/bytedance/sonic v1.8.0
go: added github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311
go: added github.com/gin-contrib/sse v0.1.0
go: added github.com/gin-gonic/gin v1.9.0
go: added github.com/go-playground/locales v0.14.1
go: added github.com/go-playground/universal-translator v0.18.1
go: added github.com/go-playground/validator/v10 v10.11.2
go: added github.com/goccy/go-json v0.10.0
go: added github.com/json-iterator/go v1.1.12
go: added github.com/klauspost/cpuid/v2 v2.0.9
go: added github.com/leodido/go-urn v1.2.1
go: added github.com/mattn/go-isatty v0.0.17
go: added github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421
go: added github.com/modern-go/reflect2 v1.0.2
go: added github.com/pelletier/go-toml/v2 v2.0.6
go: added github.com/twitchyliquid64/golang-asm v0.15.1
go: added github.com/ugorji/go/codec v1.2.9
go: added golang.org/x/arch v0.0.0-20210923205945-b76863e36670
go: added golang.org/x/crypto v0.5.0
go: added golang.org/x/net v0.7.0
go: added golang.org/x/sys v0.5.0
go: added golang.org/x/text v0.7.0
go: added google.golang.org/protobuf v1.28.1
go: added gopkg.in/yaml.v3 v3.0.1

完成後查看go.mod文件

sh
$ cat go.mod
module golearn

go 1.20

require github.com/gin-gonic/gin v1.9.0

require (
  github.com/bytedance/sonic v1.8.0 // indirect
  github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
  github.com/gin-contrib/sse v0.1.0 // indirect
  github.com/go-playground/locales v0.14.1 // indirect
  github.com/go-playground/universal-translator v0.18.1 // indirect
  github.com/go-playground/validator/v10 v10.11.2 // indirect
  github.com/goccy/go-json v0.10.0 // indirect
  github.com/json-iterator/go v1.1.12 // indirect
  github.com/klauspost/cpuid/v2 v2.0.9 // indirect
  github.com/leodido/go-urn v1.2.1 // indirect
  github.com/mattn/go-isatty v0.0.17 // indirect
  github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
  github.com/modern-go/reflect2 v1.0.2 // indirect
  github.com/pelletier/go-toml/v2 v2.0.6 // indirect
  github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
  github.com/ugorji/go/codec v1.2.9 // indirect
  golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
  golang.org/x/crypto v0.5.0 // indirect
  golang.org/x/net v0.7.0 // indirect
  golang.org/x/sys v0.5.0 // indirect
  golang.org/x/text v0.7.0 // indirect
  google.golang.org/protobuf v1.28.1 // indirect
  gopkg.in/yaml.v3 v3.0.1 // indirect
)

可以發現相較於之前多了很多東西,同時也會發現目錄下多了一個名為go.sum的文件

sh
$ ls
go.mod  go.sum  main.go

這裡先按下不表,修改main.go文件如下代碼:

go
package main

import (
  "github.com/gin-gonic/gin"
)

func main() {
  gin.Default().Run()
}

再次運行項目

sh
$ go run golearn
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

於是,通過一行代碼就運行起了一個最簡單的 Web 服務器。當不再需要某一個依賴時,也可以使用go get命令來刪除該依賴,這裡以刪除 Gin 為例子

sh
$ go get github.com/gin-gonic/gin@none
go: removed github.com/gin-gonic/gin v1.9.0

在依賴地址後面加上@none即可刪除該依賴,結果也提示了刪除成功,此時再次查看go.mod文件會發現沒有了 Gin 依賴。

sh
$ cat go.mod | grep github.com/gin-gonic/gin

當需要升級最新版本時,可以加上@latest後綴,或者可以自行查詢可用的 Release 版本號

sh
$ go get -u github.com/gin-gonic/gin@latest

安裝命令行

go install命令會將第三方依賴下載到本地並編譯成二進制文件,得益於 go 的編譯速度,這一過程通常不會花費太多時間,然後 go 會將其存放在$GOPATH/bin或者$GOBIN目錄下,以便在全局可以執行該二進制文件(前提是你將這些路徑添加到了環境變量中)。

TIP

在使用install命令時,必須指定版本號。

例如下載由 go 語言編寫的調試器delve

bash
$ go install github.com/go-delve/delve/cmd/dlv@latest
go: downloading github.com/go-delve/delve v1.22.1
go: downloading github.com/cosiner/argv v0.1.0
go: downloading github.com/derekparker/trie v0.0.0-20230829180723-39f4de51ef7d
go: downloading github.com/go-delve/liner v1.2.3-0.20231231155935-4726ab1d7f62
go: downloading github.com/google/go-dap v0.11.0
go: downloading github.com/hashicorp/golang-lru v1.0.2
go: downloading golang.org/x/arch v0.6.0
go: downloading github.com/cpuguy83/go-md2man/v2 v2.0.2
go: downloading go.starlark.net v0.0.0-20231101134539-556fd59b42f6
go: downloading github.com/cilium/ebpf v0.11.0
go: downloading github.com/mattn/go-runewidth v0.0.13
go: downloading github.com/russross/blackfriday/v2 v2.1.0
go: downloading github.com/rivo/uniseg v0.2.0
go: downloading golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2

$ dlv -v
Error: unknown shorthand flag: 'v' in -v
Usage:
  dlv [command]

Available Commands:
  attach      Attach to running process and begin debugging.
  completion  Generate the autocompletion script for the specified shell
  connect     Connect to a headless debug server with a terminal client.
  core        Examine a core dump.
  dap         Starts a headless TCP server communicating via Debug Adaptor Protocol (DAP).
  debug       Compile and begin debugging main package in current directory, or the package specified.
  exec        Execute a precompiled binary, and begin a debug session.
  help        Help about any command
  test        Compile test binary and begin debugging program.
  trace       Compile and begin tracing program.
  version     Prints version.

Additional help topics:
  dlv backend    Help about the --backend flag.
  dlv log        Help about logging flags.
  dlv redirect   Help about file redirection.

Use "dlv [command] --help" for more information about a command.

模塊管理

上述所有的內容都只是在講述 Go Mod 的基本使用,但事實上要學會 Go Mod 僅僅只有這些是完全不夠的。官方對於模塊的定義為:一組被版本標記的包集合。上述定義中,包應該是再熟悉不過的概念了,而版本則是要遵循語義化版本號,定義為:v(major).(minor).(patch)的格式,例如 Go 的版本號v1.20.1,主版本號是 1,小版本號是 20,補丁版本是 1,合起來就是v1.20.1,下面是詳細些的解釋:

  • major:當 major 版本變化時,說明項目發生了不兼容的改動,老版本的項目升級到新版本大概率沒法正常運行。
  • minor:當minor版本變化時,說明項目增加了新的特性,只是先前版本的基礎只是增加了新的功能。
  • patch:當patch版本發生變化時,說明只是有 bug 被修復了,沒有增加任何新功能。

常用命令

命令說明
go mod download下載當前項目的依賴包
go mod edit編輯 go.mod 文件
go mod graph輸出模塊依賴圖
go mod init在當前目錄初始化 go mod
go mod tidy清理項目模塊
go mod verify驗證項目的依賴合法性
go mod why解釋項目哪些地方用到了依賴
go clean -modcache用於刪除項目模塊依賴緩存
go list -m列出模塊

前往go mod cmd了解命令的更多有關信息

模塊存儲

當使用 Go Mod 進行項目管理時,模塊緩存默認存放在$GOPATH/pkg/mod目錄下,也可以修改$GOMODCACHE來指定存放在另外一個位置。

sh
$ go env -w GOMODCACHE=你的模塊緩存路徑

同一個機器上的所有 Go Module 項目共享該目錄下的緩存,緩存沒有大小限制且不會自動刪除,在緩存中解壓的依賴源文件都是只讀的,想要清空緩存可以執行如下命令。

sh
$ go clean -modcache

$GOMODCACHE/cache/download目錄下存放著依賴的原始文件,包括哈希文件,原始壓縮包等,如下例:

bash
$ ls $(go env GOMODCACHE)/cache/download/github.com/246859/hello/@v -1
list
v1.0.0.info
v1.0.0.lock
v1.0.0.mod
v1.0.0.zip
v1.0.0.ziphash

解壓過後的依賴組織形式如下所示,就是指定模塊的源代碼。

bash
$ ls $(go env GOMODCACHE)/github.com/246859/hello@v1.0.0 -1
LICENSE
README.md
cmd/
example/
go.mod
hello.go
hello_test.go

版本選擇

Go 在依賴版本選擇時,遵循最小版本選擇原則。下面是一個官網給的例子,主模塊引用了模塊 A 的 1.2 版本和模塊 B 的 1.2 版本,同時模塊 A 的 1.2 版本引用了模塊 C 的 1.3 版本,模塊 B 的 1.2 版本引用了模塊 C 的 1.4 版本,並且模塊 C 的 1.3 和 1.4 版本都同時引用了模塊 D 的 1.2 版本,根據最小可用版本原則,Go 最終會選擇的版本是 A1.2,B1.2,C1.4 和 D1.2。其中淡藍色的表示go.mod文件加載的,框選的表示最終選擇的版本。

官網中還給出了其他幾個例子,大體意思都差不多。

go.mod

每創建一個 Go Mod 項目都會生成一個go.mod文件,因此熟悉go.mod文件是非常有必要的,不過大部分情況並不需要手動的修改go.mod文件。

module golearn

go 1.20

require github.com/gin-gonic/gin v1.9.0

require (
   github.com/bytedance/sonic v1.8.0 // indirect
   github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
   github.com/gin-contrib/sse v0.1.0 // indirect
   github.com/go-playground/locales v0.14.1 // indirect
   github.com/go-playground/universal-translator v0.18.1 // indirect
   github.com/go-playground/validator/v10 v10.11.2 // indirect
   github.com/goccy/go-json v0.10.0 // indirect
   github.com/json-iterator/go v1.1.12 // indirect
   github.com/klauspost/cpuid/v2 v2.0.9 // indirect
   github.com/leodido/go-urn v1.2.1 // indirect
   github.com/mattn/go-isatty v0.0.17 // indirect
   github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
   github.com/modern-go/reflect2 v1.0.2 // indirect
   github.com/pelletier/go-toml/v2 v2.0.6 // indirect
   github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
   github.com/ugorji/go/codec v1.2.9 // indirect
   golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
   golang.org/x/crypto v0.5.0 // indirect
   golang.org/x/net v0.7.0 // indirect
   golang.org/x/sys v0.5.0 // indirect
   golang.org/x/text v0.7.0 // indirect
   google.golang.org/protobuf v1.28.1 // indirect
   gopkg.in/yaml.v3 v3.0.1 // indirect
)

在文件中可以發現絕大多數的依賴地址都帶有github等字眼,這是因為 Go 並沒有一個公共的依賴倉庫,大部分開源項目都是在托管在 Gitub 上的,也有部分的是自行搭建倉庫,例如google.golang.org/protobufgolang.org/x/crypto。通常情況下,這一串網址同時也是 Go 項目的模塊名稱,這就會出現一個問題,URL 是不分大小寫的,但是存儲依賴的文件夾是分大小寫的,所以go get github.com/gin-gonic/gingo get github.com/gin-gonic/Gin兩個引用的是同一個依賴但是本地存放的路徑不同。發生這種情況時,Go 並不會直接把大寫字母當作存放路徑,而是會將其轉義為!小寫字母,比如github.com\BurntSushi最終會轉義為github.com\!burnt!sushi

module

module關鍵字聲明了當前項目的模塊名,一個go.mod文件中只能出現一個module關鍵字。例子中的

module golearn

代表著當前模塊名為golearn,例如打開 Gin 依賴的go.mod文件可以發現它的module

module github.com/gin-gonic/gin

Gin 的模塊名就是下載依賴時使用的地址,這也是通常而言推薦模塊名格式,域名/用戶/倉庫名

TIP

有一個需要注意的點是,當主版本大於 1 時,主版本號要體現在模塊名中,例如

github.com/my/example

如果版本升級到了 v2.0.0,那麼模塊名就需要修改成如下

github.com/my/example/v2

如果原有項目引用了老版本,且新版本不加以區分的話,在引用依賴時由於路徑都一致,所以使用者並不能區分主版本變化所帶來的不兼容變動,這樣就可能會造成程序錯誤。

Deprecation

module的上一行開頭注釋Deprecated來表示該模塊已棄用,例如

// Deprecated: use example.com/mod/v2 instead.
module example.com/mod

go

go關鍵字表示了當前編寫當前項目所用到的 Go 版本,版本號必須遵循語義化規則,根據 go 版本的不同,Go Mod 會表現出不同的行為,下方是一個簡單示例,關於 Go 可用的版本號自行前往官方查閱。

go 1.20

require

require關鍵字表示引用了一個外部依賴,例如

require github.com/gin-gonic/gin v1.9.0

格式是require 模塊名 版本號,有多個引用時可以使用括號括起來

require (
   github.com/bytedance/sonic v1.8.0 // indirect
)

帶有// indirect注釋的表示該依賴沒有被當前項目直接引用,可能是項目直接引用的依賴引用了該依賴,所以對於當前項目而言就是間接引用。前面提到過主板變化時要體現在模塊名上,如果不遵循此規則的模塊被稱為不規范模塊,在require時,就會加上 incompatible 注釋。

require example.com/m v4.1.2+incompatible

偽版本

在上面的go.mod文件中,可以發現有一些依賴包的版本並不是語義化的版本號,而是一串不知所雲的字符串,這其實是對應版本的 CommitID,語義化版本通常指的是某一個 Release。偽版本號則可以細化到指定某一個 Commit,通常格式為vx.y.z-yyyyMMddHHmmss-CommitId,由於其vx.y.z並不一定真實存在,所以稱為偽版本,例如下面例子中的v0.0.0並不存在,真正有效的是其後的 12 位 CommitID。

// CommitID一般取前12位
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect

同理,在下載依賴時也可以指定 CommitID 替換語義化版本號

go get github.com/chenzhuoyu/base64x@fe3a3abad311

exclude

exclude關鍵字表示了不加載指定版本的依賴,如果同時有require引用了相同版本的依賴,也會被忽略掉。該關鍵字僅在主模塊中才生效。例如

exclude golang.org/x/net v1.2.3

exclude (
    golang.org/x/crypto v1.4.5
    golang.org/x/text v1.6.7
)

replace

replace將會替換掉指定版本的依賴,可以使用模塊路徑和版本替換又或者是其他平台指定的文件路徑,例子

text
replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5

replace (
    golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
    golang.org/x/net => example.com/fork/net v1.4.5
    golang.org/x/net v1.2.3 => ./fork/net
    golang.org/x/net => ./fork/net
)

=>左邊的版本被替換,其他版本的同一個依賴照樣可以正常訪問,無論是使用本地路徑還是模塊路徑指定替換,如果替換模塊具有 go.mod 文件,則其module指令必須與所替換的模塊路徑匹配。

retract

retract指令表示,不應該依賴retract所指定依賴的版本或版本范圍。例如在一個新的版本發布後發現了一個重大問題,這個時候就可以使用retract指令。

撤回一些版本

text
retract (
    v1.0.0 // Published accidentally.
    v1.0.1 // Contains retractions only.
)

撤回版本范圍

text
retract v1.0.0
retract [v1.0.0, v1.9.9]
retract (
    v1.0.0
    [v1.0.0, v1.9.9]
)

go.sum

go.sum文件在創建項目之初並不會存在,只有在真正引用了外部依賴後,才會生成該文件,go.sum文件並不適合人類閱讀,也不建議手動修改該文件。它的作用主要是解決一致性構建問題,即不同的人在不同的環境中使用同一個的項目構建時所引用的依賴包必須是完全相同的,這單單靠一個go.mod文件是無法保證的。

接下來看看下載一個依賴時,Go 從頭到尾都做了些什麼事,首先使用如下命令下載一個依賴

go get github.com/bytedance/sonic v1.8.0

go get 命令首先會將依賴包下載到本地的緩存目錄中,通常該目錄為$GOMODCACHE/cache/download/,該目錄根據域名來劃分不同網站的依賴包,所以你可能會看到如下的目錄結構

sh
$ ls
cloud.google.com/      go.opencensus.io/     gopkg.in/          nhooyr.io/
dmitri.shuralyov.com/  go.opentelemetry.io/  gorm.io/           rsc.io/
github.com/            go.uber.org/          honnef.co/         sumdb/
go.etcd.io/            golang.org/           lukechampine.com/
go.mongodb.org/        google.golang.org/    modernc.org/

那麼上例中下載的依賴包存放的路徑就位於

$GOMODCACHE/cache/download/github.com/bytedance/sonic/@v/

可能的目錄結構如下,會有好幾個版本命名的文件

sh
$ ls
list         v1.8.0.lock  v1.8.0.ziphash  v1.8.3.mod
v1.5.0.mod   v1.8.0.mod   v1.8.3.info     v1.8.3.zip
v1.8.0.info  v1.8.0.zip   v1.8.3.lock     v1.8.3.ziphash

通常情況下,該目錄下一定有一個list文件,用於記錄該依賴已知的版本號,而對於每一個版本而言,都會有如下的文件:

  • zip:依賴的源碼壓縮包
  • ziphash:根據依賴壓縮包所計算出的哈希值
  • info:json 格式的版本元數據
  • mod:該版本的go.mod文件
  • lock:臨時文件,官方也沒說干什麼用的

一般情況下,Go 會計算壓縮包和go.mod兩個文件的哈希值,然後再根據 GOSUMDB 所指定的服務器(默認是 sum.golang.org)查詢該依賴包的哈希值,如果本地計算出的哈希值與查詢得到的結果不一致,那麼就不會再向下執行。如果一致的話,就會更新go.mod文件,並向go.sum文件插入兩條記錄,大致如下:

github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=

TIP

假如禁用了 GOSUMDB,Go 會直接將本地計算得到的哈希值寫入go.sum文件中,一般不建議這麼做。

正常情況下每一個依賴都會有兩條記錄,第一個是壓縮包的哈希值,第二個是依賴包的go.mod文件的哈希值,記錄格式為模塊名 版本號 算法名稱:哈希值,有些比較古老的依賴包可能沒有go.mod文件,所以就不會有第二條哈希記錄。當這個項目在另一個人的環境中構建時,Go 會根據go.mod中指定的本地依賴計算哈希值,再與go.sum中記錄的哈希值進行比對,如果哈希值不一致,則說明依賴版本不同,就會拒絕構建。發生這種情況時,本地依賴和go.sum文件都有可能被修改過,但是由於go.sum是經過 GOSUMDB 查詢記錄的,所以會傾向於更相信go.sum文件。

私有模塊

Go Mod 大多數工具都是針對開源項目而言的,不過 Go 也對私有模塊進行了支持。對於私有項目而言,通常情況下需要配置以下幾個環境配置來進行模塊私有處理

  • GOPROXY :依賴的代理服務器集合
  • GOPRIVATE :私有模塊的模塊路徑前綴的通用模式列表,如果模塊名符合規則表示該模塊為私有模塊,具體行為與 GONOPROXY 和 GONOSUMDB 一致。
  • GONOPROXY :不從代理中下載的模塊路徑前綴的通用模式列表,如果符合規則在下載模塊時不會走 GOPROXY,嘗試直接從版本控制系統中下載。
  • GONOSUMDB :不進行 GOSUMDB 公共校驗的模塊路徑前綴的通用模式列表,如果符合在下載模塊校驗時不會走 checksum 的公共數據庫。
  • GOINSECURE :可以通過 HTTP 和其他不安全協議檢索的模塊路徑前綴的通用模式列表。

工作區

前面提到了go.mod文件支持replace指令,這使得我們可以暫時使用一些本地來不及發版的修改,如下所示

replace (
  github.com/246859/hello v1.0.1 => ./hello
)

在編譯時,go 就會使用本地的 hello 模塊,在日後發布新版本後再將其去掉。

但如果使用了 replace指令的話會修改go.mod文件的內容,並且該修改可能會被誤提交到遠程倉庫中,這一點是我們不希望看到的,因為replace指令所指定的 target 是一個文件路徑而非網絡 URL,這台機器上能用的路徑可能到另一台機器上就不能用了,文件路徑在跨平台方面也會是一個大問題。為了解決這類問題,工作區便應運而生。

工作區(workspace),是 Go 在 1.18 引入的關於多模塊管理的一個新的解決方案,旨在更好的進行本地的多模塊開發工作,下面將通過一個示例進行講解。

示例倉庫:246859/work: go work example (github.com)

示例

首先項目下有兩個獨立的 go 模塊,分別是authuser

bash
$ ls -1
LICENSE
README.md
auth
go.work
user

auth模塊依賴於user模塊的結構體User,內容如下

go
package auth

import (
  "errors"
  "github.com/246859/work/user"
)

// Verify user credentials if is ok
func Verify(user user.User) (bool, error) {
  password, err := query(user.Name)
  if err != nil {
    return false, err
  }
  if password != user.Password {
    return false, errors.New("authentication failed")
  }
  return true, nil
}

func query(username string) (string, error) {
  if username == "jack" {
    return "jack123456", nil
  }
  return "", errors.New("user not found")
}

user 模塊內容如下

go
package user

type User struct {
  Name     string
  Password string
  Age      int
}

在這個項目中,我們可以這樣編寫go.work文件

go 1.22

use (
  ./auth
  ./user
)

其內容非常容易理解,使用use指令,指定哪些模塊參與編譯,接下來運行 auth 模塊中的代碼

go
// auth/example/main.go
package main

import (
  "fmt"
  "github.com/246859/work/auth"
  "github.com/246859/work/user"
)

func main() {
  ok, err := auth.Verify(user.User{Name: "jack", Password: "jack123456"})
  if err != nil {
    panic(err)
  }
  fmt.Printf("%v", ok)
}

運行如下命令,通過結果得知成功導入了模塊。

bash
$ go run ./auth/example
true

在以前的版本,對於這兩個獨立的模塊,如果 auth 模塊想要使用 user 模塊中的代碼只有兩種辦法

  1. 提交 user 模塊的修改並推送到遠程倉庫,發布新版本,然後修改go.mod文件為指定版本
  2. 修改go.mod文件將依賴重定向到本地文件

兩種方法都需要修改go.mod文件,而工作區的存在就是為了能夠在不修改go.mod文件的情況下導入其它模塊。不過需要明白的一點是,go.work文件僅用在開發過程中,它的存在只是為了更加方便的進行本地開發,而不是進行依賴管理,它只是暫時讓你略過了提交到發版的這一過程,可以讓你馬上使用 user 模塊的新修改而無需進行等待,當 user 模塊測試完畢後,最後依舊需要發布新版本,並且 auth 模塊最後仍然要修改go.mod文件引用最新版本(這一過程可以用go work sync命令來完成),因此在正常的 go 開發過程中,go.work也不應該提交到 VCS 中(示例倉庫中的go.work僅用於演示),因為其內容都是依賴於本地的文件,且其功能也僅限於本地開發。

命令

下面是一些工作區的命令

命令介紹
edit編輯go.work
init初始化一個新的工作區
sync同步工作區的模塊依賴
usego.work中添加一個新模塊
vendor將依賴按照 vendor 格式進行復制

前往go work cmd了解命令的更多有關信息

指令

go.work文件的內容很簡單,只有三個指令

  • go,指定 go 版本
  • use,指定使用的模塊
  • replace,指定替換的模塊

除了use指令外,其它兩個基本上等同於go.mod中的指令,只不過go.work中的的replace指令會作用於所有的模塊,一個完整的go.work如下所示。

tex
go 1.22

use(
  ./auth
  ./user
)

repalce github.com/246859/hello v1.0.0 => /home/jack/code/hello

Golang學習網由www.golangdev.cn整理維護