模块

寒江蓑笠翁大约 51 分钟

模块

每一个现代语言都会有属于自己的一个成熟的依赖管理工具,例如Java的Gradle,Python的Pip,NodeJs的Npm等,一个好的依赖管理工具可以为开发者省去不少时间并且可以提升开发效率。然而Go在早期并没有一个成熟的依赖管理解决方案,那时所有的代码都存放在GOPATH目录下,对于工程项目而言十分的不友好,版本混乱,依赖难以管理,为了解决这个问题,各大社区开发者百家争鸣,局面一时间混乱了起来,期间也不乏出现了一些佼佼者例如Vendor,直到Go1.11官方终于推出了Go Mod这款官方的依赖管理工具,结束了先前的混乱局面,并在后续的更新中不断完善,淘汰掉了曾经老旧的工具。时至今日,在撰写本文时,Go发行版本已经到了1.20,在今天几乎所有的Go项目都在采用Go Mod,所以在本文也只会介绍Go Mod,官方对于Go模块也编写了非常细致的文档:Go Modules Referenceopen in new window


编写模块

Go Module本质上是基于VCS(版本控制系统),当你在下载依赖时,实际上执行的是VCS命令,比如git,所以如果你想要分享你编写的库,只需要做到以下三点:

  • 源代码仓库可公开访问,且VCS属于以下的其中之一
    • git
    • hg (Mercurial)
    • bzr (Bazaar)
    • svn
    • fossil
  • 是一个符合规范的go mod项目
  • 符合语义化版本规范

所以你只需要正常使用VCS开发,并为你的特定版本打上符合标准的Tag,其它人就可以通过模块名来下载你所编写的库,下面将通过示例来演示进行模块开发的几个步骤。

示例仓库:246859/hello: say hello (github.com)open in new window

准备

在开始之前确保你的版本足以完全支持go mod(go >= 1.17),并且启用了Go Module,通过如下命令来查看是否开启

$ go env GO111MODULE

如果未开启,通过如下命令开启用Go Module

$ go env -w GO111MODULE=on

创建

首先你需要一个可公网访问的源代码仓库,这个有很多选择,我比较推荐Github。在上面创建一个新项目,将其取名为hello,仓库名虽然没有什么特别限制,但建议还是不要使用特殊字符,因为这会影响到模块名。

创建完成后,可以看到仓库的URL是https://github.com/246859/hello,对应的go模块名就是github.com/246859/hello

然后将其克隆到本地,通过go mod init命令初始化模块。

$ 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

编写

然后就可以进行开发工作了,它的功能非常简单,只有一个函数

// 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)
}

顺便写一个测试文件进行单元测试

// 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/目录下,然后在其中编写相关代码。

// 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())
	}
}

测试

编写完后对源代码格式化并测试

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

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

运行命令行程序

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

文档

最后的最后,需要为这个库编写简洁明了的README,让其它开发者看一眼就知道怎么使用

# 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文档,你也可以自己进行丰富。

上传

当一切代码都编写并测试完毕过后,就可以将修改提交并推送到远程仓库。

$ 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

$ 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

最后再推送到远程仓库

$ 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引用库

$ 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安装命令行程序

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

或者使用go run直接运行

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

当一个库被引用过后,Go Packageopen in new window便会为其创建一个页面,这个过程是自动完成的,不需要开发者做什么工作,比如hello库就有一个专属的文档页面,如下图所示。

关于上传模块的更多详细信息,前往Add a packageopen in new window

关于如何删除模块的信息,前往Removing a packageopen in new window

设置代理

Go虽然没有像Maven Repo,PyPi,NPM这样类似的中央仓库,但是有一个官方的代理仓库:Go modules services (golang.org)open in new window,它会根据版本及模块名缓存开发者下载过的模块。不过由于其服务器部署在国外,访问速度对于国内的用户不甚友好,所以我们需要修改默认的模块代理地址,目前国内做的比较好的有以下几家:

这里选择七牛云的代理,执行如下命令来修改Go代理,其中的direct表示代理下载失败后绕过代理缓存直接访问源代码仓库。

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

代理修改成功后,日后下载依赖就会非常的迅速。


下载依赖

修改完代理后,接下来安装一个第三方依赖试试,Go官方有专门的依赖查询网站:Go Packagesopen in new window

代码引用

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

这里会出现很多搜索结果,在使用第三方依赖时,需要结合引用次数和更新时间来决定是否采用该依赖,这里直接选择第一个

进入对应的页面后,可以看出这是该依赖的一个文档页面,有着非常多关于它的详细信息,后续查阅文档时也可以来这里。

这里只需要将它的地址复制下来,然后在之前创建的项目下使用go get命令,命令如下

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

过程中会下载很多的依赖,只要没有报错就说明下载成功。

$ 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文件

$ 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的文件

$ ls
go.mod  go.sum  main.go

这里先按下不表,修改main.go文件如下代码:

package main

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

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

再次运行项目

$ 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为例子

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

在依赖地址后面加上@none即可删除该依赖,结果也提示了删除成功,此时再次查看go.mod文件会发现没有了Gin依赖。

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

当需要升级最新版本时,可以加上@latest后缀,或者可以自行查询可用的Release版本号

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

安装命令行

go install命令会将第三方依赖下载到本地并编译成二进制文件,得益于go的编译速度,这一过程通常不会花费太多时间,然后go会将其存放在$GOPATH/bin或者$GOBIN目录下,以便在全局可以执行该二进制文件(前提是你将这些路径添加到了环境变量中)。

提示

在使用install命令时,必须指定版本号。

例如下载由go语言编写的调试器delve

$ 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来指定存放在另外一个位置。

$ go env -w GOMODCACHE=你的模块缓存路径

同一个机器上的所有Go Module项目共享该目录下的缓存,缓存没有大小限制且不会自动删除,在缓存中解压的依赖源文件都是只读的,想要清空缓存可以执行如下命令。

$ go clean -modcache

$GOMODCACHE/cache/download目录下存放着依赖的原始文件,包括哈希文件,原始压缩包等,如下例:

$ 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

解压过后的依赖组织形式如下所示,就是指定模块的源代码。

$ 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文件加载的,框选的表示最终选择的版本。

官网中还给出了其他几个例子open in new window,大体意思都差不多。


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的模块名就是下载依赖时使用的地址,这也是通常而言推荐模块名格式,域名/用户/仓库名

提示

有一个需要注意的点是,当主版本大于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将会替换掉指定版本的依赖,可以使用模块路径和版本替换又或者是其他平台指定的文件路径,例子

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指令。

撤回一些版本

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

撤回版本范围

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/,该目录根据域名来划分不同网站的依赖包,所以你可能会看到如下的目录结构

$ 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/

可能的目录结构如下,会有好几个版本命名的文件

$ 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.orgopen in new window)查询该依赖包的哈希值,如果本地计算出的哈希值与查询得到的结果不一致,那么就不会再向下执行。如果一致的话,就会更新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=

提示

假如禁用了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)open in new window

示例

首先项目下有两个独立的go模块,分别是authuser

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

auth模块依赖于user模块的结构体User,内容如下

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模块内容如下

package user

type User struct {
	Name     string
	Password string
	Age      int
}

在这个项目中,我们可以这样编写go.work文件

go 1.22

use (
	./auth
	./user
)

其内容非常容易理解,使用use指令,指定哪些模块参与编译,接下来运行auth模块中的代码

// 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)
}

运行如下命令,通过结果得知成功导入了模块。

$ 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如下所示。

go 1.22

use(
	./auth
	./user
)

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