目录

Go module介绍

Go module相关知识点介绍。

Go版本下载:通过中国大陆的镜像站点 golang.google.cn/dl 来下载

Go 项目结构

Go 可执行程序项目的典型结构布局

可执行程序项目是以构建可执行程序为目的的项目,Go 社区针对这类 Go 项目所形成的典型结构布局是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

$tree -F exe-layout 
exe-layout
├── cmd/
│   ├── app1/
│   │   └── main.go
│   └── app2/
│       └── main.go
├── go.mod
├── go.sum
├── internal/
│   ├── pkga/
│   │   └── pkg_a.go
│   └── pkgb/
│       └── pkg_b.go
├── pkg1/
│   └── pkg1.go
├── pkg2/
│   └── pkg2.go
└── vendor/

如果 Go 可执行程序项目有一个且只有一个可执行程序要构建,那就比较好办了,我们可以将上面项目布局进行简化:

1
2
3
4
5
6
7
8
9

$tree -F -L 1 single-exe-layout
single-exe-layout
├── go.mod
├── internal/
├── main.go
├── pkg1/
├── pkg2/
└── vendor/

Go 库项目的典型结构布局

Go 库项目仅对外暴露 Go 包,这类项目的典型布局形式是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

$tree -F lib-layout 
lib-layout
├── go.mod
├── internal/
│   ├── pkga/
│   │   └── pkg_a.go
│   └── pkgb/
│       └── pkg_b.go
├── pkg1/
│   └── pkg1.go
└── pkg2/
    └── pkg2.go

我们看到,库类型项目相比于 Go 可执行程序项目的布局要简单一些。因为这类项目不需要构建可执行程序,所以去除了 cmd 目录。

而且,在这里,vendor 也不再是可选目录了。对于库类型项目而言,我们并不推荐在项目中放置 vendor 目录去缓存库自身的第三方依赖,库项目仅通过 go.mod 文件明确表述出该项目依赖的 module 或包以及版本要求就可以了。

Go 库项目的初衷是为了对外部(开源或组织内部公开)暴露 API,对于仅限项目内部使用而不想暴露到外部的包,可以放在项目顶层的 internal 目录下面。当然 internal 也可以有多个并存在于项目结构中的任一目录层级中,关键是项目结构设计人员要明确各级 internal 包的应用层次和范围。

对于有一个且仅有一个包的 Go 库项目来说,我们也可以将上面的布局做进一步简化,简化的布局如下所示:

1
2
3
4
5
6
7

$tree -L 1 -F single-pkg-lib-layout
single-pkg-lib-layout
├── feature1.go
├── feature2.go
├── go.mod
└── internal/

包依赖管理

Go 程序由 Go 包组合而成的**,Go 程序的构建过程就是确定包版本、编译包以及将编译后得到的目标文件链接在一起的过程。**

Go 语言的构建模式历经了三个迭代和演化过程,分别是最初期的 GOPATH、1.5 版本的 Vendor 机制,以及现在的 Go Module。

GOPATH

在这种构建模式下,Go 编译器可以在本地 GOPATH 环境变量配置的路径下,搜寻 Go 程序依赖的第三方包。如果存在,就使用这个本地包进行编译;如果不存在,就会报编译错误。

1
2
3
4
5

$go build main.go
main.go:3:8: cannot find package "github.com/sirupsen/logrus" in any of:
  /Users/tonybai/.bin/go1.10.8/src/github.com/sirupsen/logrus (from $GOROOT)
  /Users/tonybai/Go/src/github.com/sirupsen/logrus (from $GOPATH)

Go vendor

Go 核心团队在 Go 1.5 版本中做了第一次改进。增加了 vendor 构建机制,也就是 Go 源码的编译可以不在 GOPATH 环境变量下面搜索依赖包的路径,而在 vendor 目录下查找对应的依赖包。

Go 语言项目自身也在 Go 1.6 版本中增加了 vendor 目录以支持 vendor 构建,但 vendor 目录并没有实质性缓存任何第三方包。直到 Go 1.7 版本,Go 才真正在 vendor 下缓存了其依赖的外部包。这些依赖包主要是 golang.org/x 下面的包,这些包同样是由 Go 核心团队维护的,并且其更新速度不受 Go 版本发布周期的影响。

vendor 机制与目录的引入,让 Go 项目第一次具有了可重现构建(Reproducible Build)的能力。

Go Module 机制也保留了 vendor 目录(通过 go mod vendor 可以生成 vendor 下的依赖包,通过 go build -mod=vendor 可以实现基于 vendor 的构建)。

1
go mod vendor

示例:

1
2
3
4
5
#hellomodule
$ go mod vendor
$ ls
go.mod  go.sum  main.exe*  main.go  vendor/

Go 在 1.5 版本中引入 vendor 机制。vendor 机制本质上就是在 Go 项目的某个特定目录下,将项目的所有依赖包缓存起来,这个特定目录名就是 vendor。

Go 编译器会优先感知和使用 vendor 目录下缓存的第三方包版本,而不是 GOPATH 环境变量所配置的路径下的第三方包版本。这样,无论第三方依赖包自己如何变化,无论 GOPATH 环境变量所配置的路径下的第三方包是否存在、版本是什么,都不会影响到 Go 程序的构建。

如果你将 vendor 目录和项目源码一样提交到代码仓库,那么其他开发者下载你的项目后,就可以实现可重现的构建。因此,如果使用 vendor 机制管理第三方依赖包,最佳实践就是将 vendor 一并提交到代码仓库中。

面这个目录结构就是为上面的代码示例添加 vendor 目录后的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.
├── main.go
└── vendor/
    ├── github.com/
    │   └── sirupsen/
    │       └── logrus/
    └── golang.org/
        └── x/
            └── sys/
                └── unix/

在添加完 vendor 后,我们重新编译 main.go,这个时候 Go 编译器就会在 vendor 目录下搜索程序依赖的 logrus 包以及后者依赖的 golang.org/x/sys/unix 包了。

这里你还要注意一点,要想开启 vendor 机制,你的 Go 项目必须位于 GOPATH 环境变量配置的某个路径的 src 目录下面。如果不满足这一路径要求,那么 Go 编译器是不会理会 Go 项目目录下的 vendor 目录的。

Go Module

一个 Go Module 是一个 Go 包的集合。module 是有版本的,所以 module 下的包也就有了版本属性。这个 module 与这些包会组成一个独立的版本单元,它们一起打版本、发布和分发。

在 Go Module 模式下,通常一个代码仓库对应一个 Go Module。一个 Go Module 的顶层目录下会放置一个 go.mod 文件,每个 go.mod 文件会定义唯一一个 module,也就是说 Go Module 与 go.mod 是一一对应的。

go.mod 文件所在的顶层目录也被称为 module 的根目录,module 根目录以及它子目录下的所有 Go 包均归属于这个 Go Module,这个 module 也被称为 main module。

将基于当前项目创建一个 Go Module,通常有如下几个步骤:

  1. 第一步,通过 go mod init 创建 go.mod 文件,将当前项目变为一个 Go Module;
  2. 第二步,通过 go mod tidy 命令自动更新当前 module 的依赖信息;
  3. 第三步,执行 go build,执行新 module 的构建。

demo示例

1
2
3
4
5
6
7
8

package main

import "github.com/sirupsen/logrus"

func main() {
  logrus.Println("hello, go module mode")
}
1
2
3
4
5

$go mod init github.com/bigwhite/module-mode
go: creating new go.mod: module github.com/bigwhite/module-mode
go: to add module requirements and sums:
  go mod tidy

现在,go mod init 在当前项目目录下创建了一个 go.mod 文件,这个 go.mod 文件将当前项目变为了一个 Go Module,项目根目录变成了 module 根目录。go.mod 的内容是这样的:

1
2
3
4

module github.com/bigwhite/module-mode

go 1.16

这个 go.mod 文件现在处于初始状态,它的第一行内容用于声明 module 路径 (module path),最后一行是一个 Go 版本指示符,用于表示这个 module 是在某个特定的 Go 版本的 module 语义的基础上编写的。

go mod init 命令还输出了两行日志,提示我们可以使用 go mod tidy 命令,添加 module 依赖以及校验和。go mod tidy 命令会扫描 Go 源码,并自动找出项目依赖的外部 Go Module 以及版本,下载这些依赖并更新本地的 go.mod 文件。我们按照这个提示执行一下 go mod tidy 命令:

1
2
3
4
5
6
7

$go mod tidy
go: finding module for package github.com/sirupsen/logrus
go: downloading github.com/sirupsen/logrus v1.8.1
go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.8.1
go: downloading golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
go: downloading github.com/stretchr/testify v1.2.2

我们看到,对于一个处于初始状态的 module 而言,go mod tidy 分析了当前 main module 的所有源文件,找出了当前 main module 的所有第三方依赖,确定第三方依赖的版本,还下载了当前 main module 的直接依赖包(比如 logrus),以及相关间接依赖包(直接依赖包的依赖,比如上面的 golang.org/x/sys 等)。

Go Module 还支持通过 Go Module 代理服务加速第三方依赖的下载。提到过 GOPROXY 环境变量,这个环境变量的默认值为“https: // proxy.golang.org,direct”,不过我们可以配置更适合于中国大陆地区的 Go Module 代理服务。如:GOPROXY=https://goproxy.cn

由 go mod tidy 下载的依赖 module 会被放置在本地的 module 缓存路径下,默认值为 $GOPATH[0]/pkg/mod,Go 1.15 及以后版本可以通过 GOMODCACHE 环境变量,自定义本地 module 的缓存路径。

1
2
$ go env |grep GOMODCACHE
set GOMODCACHE=D:\GO_projects\pkg\mod

执行 go mod tidy 后,我们示例 go.mod 的内容更新如下:

1
2
3
4
5
6

module github.com/bigwhite/module-mode

go 1.16

require github.com/sirupsen/logrus v1.8.1

你可以看到,当前 module 的直接依赖 logrus,还有它的版本信息都被写到了 go.mod 文件的 require 段中。

而且,执行完 go mod tidy 后,当前项目除了 go.mod 文件外,还多了一个新文件 go.sum,内容是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

这同样是由 go mod 相关命令维护的一个文件,它存放了特定版本 module 内容的哈希值。

这是 Go Module 的一个安全措施。当将来这里的某个 module 的特定版本被再次下载的时候,go 命令会使用 go.sum 文件中对应的哈希值,和新下载的内容的哈希值进行比对,只有哈希值比对一致才是合法的,这样可以确保你的项目所依赖的 module 内容,不会被恶意或意外篡改。因此,我推荐你把 go.mod 和 go.sum 两个文件与源码,一并提交到代码版本控制服务器上。

接下来,我们只需在当前 module 的根路径下,执行 go build 就可以完成 module 的构建了!

go build 命令会读取 go.mod 中的依赖及版本信息,并在本地 module 缓存路径下找到对应版本的依赖 module,执行编译和链接。如果顺利的话,我们会在当前目录下看到一个新生成的可执行文件 module-mode,执行这个文件我们就能得到正确结果了。

整个过程的执行步骤是这样的:

1
2
3
4
5
6

$go build
$ls
go.mod    go.sum    main.go    module-mode*
$./module-mode 
INFO[0000] hello, go module mode
  1. Go Module 的语义导入版本机制

    按照语义版本规范,主版本号不同的两个版本是相互不兼容的。而且,在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本。补丁版本号也不影响兼容性。

    而且,Go Module 规定:如果同一个包的新旧版本是兼容的,那么它们的包导入路径应该是相同的

    语义导入版本机制是 Go Moudle 其他机制的基础,它是通过在包导入路径中引入主版本号的方式,来区别同一个包的不兼容版本。

  2. Go Module 的最小版本选择原则

    所以 Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。

1
2
3
4
5

import (
    "github.com/sirupsen/logrus"
    logv2 "github.com/sirupsen/logrus/v2"
)

思考题是:如何将基于 GOPATH 构建模式的现有项目迁移为使用 Go Module 构建模式?

1
2
3
4
if go version < 1.13: 项目移出GOPATH/src
go env -w GO111MODULE=on
go mod init module_path
go mod tidy

依赖管理

依赖管理的常用场景介绍

为当前 module 添加一个依赖

如何为一个 Go Module 添加一个新的依赖包呢?

如果我们要为这个项目增加一个新依赖:github.com/google/uuid

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

package main

import (
    // new package uuid
  "github.com/google/uuid"
  "github.com/sirupsen/logrus"
)

func main() {
  logrus.Println("hello, go module mode")
  logrus.Println(uuid.NewString())
}

执行命令

1
go mod tidy

go module会更新mod文件,并下载依赖包

1
2
3
4

$go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.3.0

升级 / 降级依赖的版本

Go Module 的版本号采用了语义版本规范,也就是版本号使用 vX.Y.Z 的格式。其中 X 是主版本号,Y 为次版本号 (minor),Z 为补丁版本号 (patch)。主版本号相同的两个版本,较新的版本是兼容旧版本的。如果主版本号不同,那么两个版本是不兼容的。

以上面提到过的 logrus 为例,logrus 现在就存在着多个发布版本,我们可以通过下面命令来进行查询:

1
2
3

$go list -m -versions github.com/sirupsen/logrus
github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1

在这个例子中,基于初始状态执行的 go mod tidy 命令,帮我们选择了 logrus 的最新发布版本 v1.8.1。如果你觉得这个版本存在某些问题,想将 logrus 版本降至某个之前发布的兼容版本,比如 v1.7.0,

有2种方式可以选择:

  1. 那么我们可以在项目的 module 根目录下,执行带有版本号的 go get 命令:
1
2
3
4

$go get github.com/sirupsen/logrus@v1.7.0
go: downloading github.com/sirupsen/logrus v1.7.0
go get: downgraded github.com/sirupsen/logrus v1.8.1 => v1.7.0

从这个执行输出的结果,我们可以看到,go get 命令下载了 logrus v1.7.0 版本,并将 go.mod 中对 logrus 的依赖版本从 v1.8.1 降至 v1.7.0。

  1. 当然我们也可以使用万能命令 go mod tidy 来帮助我们降级,但前提是首先要用 go mod edit 命令,明确告知我们要依赖 v1.7.0 版本,而不是 v1.8.1,这个执行步骤是这样的:
1
2
3
4

$go mod edit -require=github.com/sirupsen/logrus@v1.7.0
$go mod tidy       
go: downloading github.com/sirupsen/logrus v1.7.0

添加一个主版本号大于 1 的依赖

语义导入版本机制有一个原则:如果新旧版本的包使用相同的导入路径,那么新包与旧包是兼容的。也就是说,如果新旧两个包不兼容,那么我们就应该采用不同的导入路径。

按照语义版本规范,如果我们要为项目引入主版本号大于 1 的依赖,比如 v2.0.0,那么由于这个版本与 v1、v0 开头的包版本都不兼容,我们在导入 v2.0.0 包时,不能再直接使用 github.com/user/repo,而要使用像下面代码中那样不同的包导入路径:

1
2

import github.com/user/repo/v2/xxx

也就是说,如果我们要为 Go 项目添加主版本号大于 1 的依赖,我们就需要使用“语义导入版本”机制,在声明它的导入路径的基础上,加上版本号信息。我们以“向 module-mode 项目添加 github.com/go-redis/redis 依赖包的 v7 版本”为例,看看添加步骤。

首先,我们在源码中,以空导入的方式导入 v7 版本的 github.com/go-redis/redis 包:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

package main

import (
    // 添加 github.com/go-redis/redis 依赖包的 v7 版本
  _ "github.com/go-redis/redis/v7"
  "github.com/google/uuid"
  "github.com/sirupsen/logrus"
)

func main() {
  logrus.Println("hello, go module mode")
  logrus.Println(uuid.NewString())
}

接下来的步骤就与添加兼容依赖一样,我们通过 go get 或者 go mod edit + tidy 获取 redis 的 v7 版本:

1
2
3
4
5

$go get github.com/go-redis/redis/v7
go: downloading github.com/go-redis/redis/v7 v7.4.1
go: downloading github.com/go-redis/redis v6.15.9+incompatible
go get: added github.com/go-redis/redis/v7 v7.4.1

升级依赖版本到一个不兼容版本

按照语义导入版本的原则,不同主版本的包的导入路径是不同的。所以,同样地,我们这里也需要先将代码中 redis 包导入路径中的版本号改为 v8:

1
2
3
4
5
6
7

import (
    // 将代码中 redis 包导入路径中的版本号改为 v8
  _ "github.com/go-redis/redis/v8"
  "github.com/google/uuid"
  "github.com/sirupsen/logrus"
)

接下来的步骤就与添加兼容依赖一样,我们通过 go get 或者 go mod edit + tidy 获取 redis 的 v8版本

1
2
3
4
5
6

$go get github.com/go-redis/redis/v8
go: downloading github.com/go-redis/redis/v8 v8.11.1
go: downloading github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
go: downloading github.com/cespare/xxhash/v2 v2.1.1
go get: added github.com/go-redis/redis/v8 v8.11.1

移除一个依赖

代码中删除go-redis/redis/v8,通过 go list 命令列出当前 module 的所有依赖,你也会发现 go-redis/redis/v8 仍出现在结果中:

1
2
3
4
5
6
7
8
9

$go list -m all
github.com/bigwhite/module-mode
github.com/cespare/xxhash/v2 v2.1.1
github.com/davecgh/go-spew v1.1.1
... ...
github.com/go-redis/redis/v8 v8.11.1
... ...
gopkg.in/yaml.v2 v2.3.0

用 go mod tidy 命令,将这个依赖项彻底从 Go Module 构建上下文中清除掉。go mod tidy 会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除

1
go mod tidy

特殊情况:使用 vendor

Go Module 构建模式下,我们再也无需手动维护 vendor 目录下的依赖包了,Go 提供了可以快速建立和更新 vendor 的命令,我们还是以前面的 module-mode 项目为例,通过下面命令为该项目建立 vendor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

$go mod vendor
$tree -LF 2 vendor
vendor
├── github.com/
│   ├── google/
│   ├── magefile/
│   └── sirupsen/
├── golang.org/
│   └── x/
└── modules.txt

go mod vendor 命令在 vendor 目录下,创建了一份这个项目的依赖包的副本,并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。

如果我们要基于 vendor 构建,而不是基于本地缓存的 Go Module 构建,我们需要在 go build 后面加上 -mod=vendor 参数。

在 Go 1.14 及以后版本中,如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非你给 go build 传入 -mod=mod 的参数。

小结

关于 go module

  • Go 包是 Go 语言的基本组成单元。一个 Go 程序就是一组包的集合,所有 Go 代码都位于包中;
  • Go 源码可以导入其他 Go 包,并使用其中的导出语法元素,包括类型、变量、函数、方法等,而且,main 函数是整个 Go 应用的入口函数;
  • Go 源码需要先编译,再分发和运行。如果是单 Go 源文件的情况,我们可以直接使用 go build 命令 +Go 源文件名的方式编译。不过,对于复杂的 Go 项目,我们需要在 Go Module 的帮助下完成项目的构建。

关于项目结构

  1. 首先,对于以生产可执行程序为目的的 Go 项目,它的典型项目结构分为五部分:
  • 放在项目顶层的 Go Module 相关文件,包括 go.mod 和 go.sum;
  • cmd 目录:存放项目要编译构建的可执行文件所对应的 main 包的源码文件;
  • 项目包目录:每个项目下的非 main 包都“平铺”在项目的根目录下,每个目录对应一个 Go 包;
  • internal 目录:存放仅项目内部引用的 Go 包,这些包无法被项目之外引用;
  • vendor 目录:这是一个可选目录,为了兼容 Go 1.5 引入的 vendor 构建模式而存在的。这个目录下的内容均由 Go 命令自动维护,不需要开发者手工干预
  1. 第二,对于以生产可复用库为目的的 Go 项目,它的典型结构则要简单许多,我们可以直接理解为在 Go 可执行程序项目的基础上去掉 cmd 目录和 vendor 目录。

  2. 最后,早期接纳 Go 语言的开发者所建立的项目的布局深受 Go 创世项目 1.4 版本之前布局的影响,将可导出的公共包放入单独的 pkg 目录下,我们了解这种情况即可。对于新建 Go 项目,我依旧建议你采用前面介绍的标准布局形式。

    4. 依赖管理
    
  • 通过 go get 我们可以升级或降级某依赖的版本,如果升级或降级前后的版本不兼容,这里千万注意别忘了变化包导入路径中的版本号,这是 Go 语义导入版本机制的要求;
  • 通过 go mod tidy,我们可以自动分析 Go 源码的依赖变更,包括依赖的新增、版本变更以及删除,并更新 go.mod 中的依赖信息。
  • 通过 go mod vendor,我们依旧可以支持 vendor 机制,并且可以对 vendor 目录下缓存的依赖包进行自动管理。

go mod相关命令

进入到项目包含"go.mod"文件的目录路径,执行下面命令,进行项目依赖包本地化。

1
2
3
4
5
6
7
export GO111MODULE="on"

export GOPROXY=https://goproxy.cn

go mod tidy

go mod vendor

实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

### src/github.com/kubeflow/training-operator (master)
$ go mod tidy
go: downloading k8s.io/code-generator v0.19.9
go: downloading github.com/kubeflow/common v0.4.1
go: downloading sigs.k8s.io/controller-runtime v0.7.2
go: downloading github.com/google/go-cmp v0.5.4
go: downloading golang.org/x/sys v0.0.0-20210510120138-977fb7262007
go: downloading k8s.io/apiextensions-apiserver v0.19.2
go: downloading github.com/Azure/go-autorest/autorest v0.9.6
go: downloading golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f
go: downloading honnef.co/go/tools v0.0.1-2019.2.3
go: downloading k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14
go: downloading cloud.google.com/go v0.51.0
go: downloading github.com/Azure/go-autorest/autorest/adal v0.8.2
go: downloading github.com/Azure/go-autorest/tracing v0.5.0
go: downloading github.com/Azure/go-autorest/autorest/mocks v0.3.0
go: downloading github.com/Azure/go-autorest/logger v0.1.0
go: downloading github.com/Azure/go-autorest/autorest/date v0.2.0

#### github.com/kubeflow/training-operator (master)
$ go mod vendor

go mod模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

# cd project_path

export GO111MODULE="on"

export GOPROXY=https://goproxy.cn

# go mod init module_path

go mod init github.com/bingerambo/go-file-json-server

go mod tidy

go build

go list -m -json all

附录

go env

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

#set GO111MODULE=on
#set GOPROXY=https://goproxy.io

#export GO111MODULE="off"
export GO111MODULE="on"
export GOPROXY=https://goproxy.cn

#go mod init module_path

go mod init github.com/bingerambo/file_notify

go mod tidy

go build

go list -m -json all





查看下版本依赖

go list -m -json all

 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

{
        "Path": "github.com/bingerambo/file_notify",
        "Main": true,
        "Dir": "D:\\GO_projects\\src\\github.com\\bingerambo\\file_notify",
        "GoMod": "D:\\GO_projects\\src\\github.com\\bingerambo\\file_notify\\go.mod",
        "GoVersion": "1.12"
}
{
        "Path": "github.com/fsnotify/fsnotify",
        "Version": "v1.5.1",
        "Time": "2021-08-24T19:33:57Z",
        "Dir": "D:\\GO_projects\\pkg\\mod\\github.com\\fsnotify\\fsnotify@v1.5.1",
        "GoMod": "D:\\GO_projects\\pkg\\mod\\cache\\download\\github.com\\fsnotify\\fsnotify\\@v\\v1.5.1.mod",
        "GoVersion": "1.13"
}
{
        "Path": "github.com/howeyc/fsnotify",
        "Version": "v0.9.0",
        "Time": "2014-02-27T14:46:22Z",
        "Dir": "D:\\GO_projects\\pkg\\mod\\github.com\\howeyc\\fsnotify@v0.9.0",
        "GoMod": "D:\\GO_projects\\pkg\\mod\\cache\\download\\github.com\\howeyc\\fsnotify\\@v\\v0.9.0.mod"
}
{
        "Path": "golang.org/x/sys",
        "Version": "v0.0.0-20210630005230-0f9fa26af87c",
        "Time": "2021-06-30T00:52:30Z",
        "Indirect": true,
        "Dir": "D:\\GO_projects\\pkg\\mod\\golang.org\\x\\sys@v0.0.0-20210630005230-0f9fa26af87c",
        "GoMod": "D:\\GO_projects\\pkg\\mod\\cache\\download\\golang.org\\x\\sys\\@v\\v0.0.0-20210630005230-0f9fa26af87c.mod",
        "GoVersion": "1.17"
}

demo程序

我们创建一个新项目“hellomodule”,在新项目中我们将使用两个第三方库,zap 和 fasthttp,给 go build 的构建过程增加一些难度。和“hello,world”示例一样,我们通过下面命令创建“hellomodule”项目:

1
2
3
$cd ~/goprojects
$mkdir hellomodule
$cd hellomodule

接着,我们在“hellomodule“下创建并编辑我们的示例源码文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
  "github.com/valyala/fasthttp"
  "go.uber.org/zap"
)

var logger *zap.Logger

func init() {
  logger, _ = zap.NewProduction()
}

func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
  logger.Info("hello, go module", zap.ByteString("uri", ctx.RequestURI()))
}

func main() {
  fasthttp.ListenAndServe(":8081", fastHTTPHandler)
}

我们尝试一下使用编译“hello,world”的方法来编译“hellomodule”中的 main.go 源文件,go 编译器的输出结果是这样的:

Go module 构建模式是在 Go 1.11 版本正式引入的,为的是彻底解决 Go 项目复杂版本依赖的问题,在 Go 1.16 版本中,Go module 已经成为了 Go 默认的包依赖管理机制和 Go 源码构建机制。

1
2
3
$go build main.go
main.go:4:2: no required module provides package github.com/valyala/fasthttp: go.mod file not found in current directory or any parent directory; see 'go help modules'
main.go:5:2: no required module provides package go.uber.org/zap: go.mod file not found in current directory or any parent directory; see 'go help modules'

Go Module 的核心是一个名为 go.mod 的文件,在这个文件中存储了这个 module 对第三方依赖的全部信息。接下来,我们就通过下面命令为“hello,module”这个示例程序添加 go.mod 文件:

在hellomodule目录下执行命令:

1
2
3
4
$go mod init github.com/bigwhite/hellomodule
go: creating new go.mod: module github.com/bigwhite/hellomodule
go: to add module requirements and sums:
  go mod tidy

你会看到,go mod init 命令的执行结果是在当前目录下生成了一个 go.mod 文件:

1
2
3
4
$cat go.mod
module github.com/bigwhite/hellomodule

go 1.16

其实,一个 module 就是一个包的集合,这些包和 module 一起打版本、发布和分发。go.mod 所在的目录被我们称为它声明的 module 的根目录。

不过呢,这个时候的 go.mod 文件内容还比较简单,第一行内容是用于声明 module 路径(module path)的。而且,module 隐含了一个命名空间的概念,module 下每个包的导入路径都是由 module path 和包所在子目录的名字结合在一起构成。

比如,如果 hellomodule 下有子目录 pkg/pkg1,那么 pkg1 下面的包的导入路径就是由 module path(github.com/bigwhite/hellomodule)和包所在子目录的名字(pkg/pkg1)结合而成,也就是 github.com/bigwhite/hellomodule/pkg/pkg1。

备注说明:也就是自己项目引用自己的pkg时可以使用

1
 import github.com/bigwhite/hellomodule/pkg/pkg1 

即模块名+路径方式获取,无需使用gopath

另外,go.mod 的最后一行是一个 Go 版本指示符,用于表示这个 module 是在某个特定的 Go 版本的 module 语义的基础上编写的。

有了 go.mod 后,是不是我们就可以构建 hellomodule 示例了呢?

来试试看!我们执行一下构建,Go 编译器输出结果是这样的:

1
2
3
4
5
$go build main.go
main.go:4:2: no required module provides package github.com/valyala/fasthttp; to add it:
  go get github.com/valyala/fasthttp
main.go:5:2: no required module provides package go.uber.org/zap; to add it:
  go get go.uber.org/zap

你会看到,Go 编译器提示源码依赖 fasthttp 和 zap 两个第三方包,但是 go.mod 中没有这两个包的版本信息,我们需要按提示手工添加信息到 go.mod 中。

这个时候,除了按提示手动添加外,我们也可以使用 go mod tidy 命令,让 Go 工具自动添加:

1
2
3
4
5
$go mod tidy       
go: downloading go.uber.org/zap v1.18.1
go: downloading github.com/valyala/fasthttp v1.28.0
go: downloading github.com/andybalholm/brotli v1.0.2
... ...

从输出结果中,我们看到 Go 工具不仅下载并添加了 hellomodule 直接依赖的 zap 和 fasthttp 包的信息,还下载了这两个包的相关依赖包。go mod tidy 执行后,我们 go.mod 的最新内容变成了这个样子:

1
2
3
4
5
6
7
8
module github.com/bigwhite/hellomodule

go 1.16

require (
  github.com/valyala/fasthttp v1.28.0
  go.uber.org/zap v1.18.1
)

这个时候,go.mod 已经记录了 hellomodule 直接依赖的包的信息。不仅如此,hellomodule 目录下还多了一个名为 go.sum 的文件,这个文件记录了 hellomodule 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。在构建的时候,如果本地依赖包的 hash 值与 go.sum 文件中记录的不一致,就会被拒绝构建。

有了 go.mod 以及 hellomodule 依赖的包版本信息后,我们再来执行构建:

1
2
3
$go build main.go
$ls
go.mod    go.sum    main*    main.go

这次我们成功构建出了可执行文件 main,运行这个文件,新开一个终端窗口,在新窗口中使用 curl 命令访问该 http 服务:curl localhost:8081/foo/bar,我们就会看到服务端输出如下日志:

1
2
$./main
{"level":"info","ts":1626614126.9899719,"caller":"hellomodule/main.go:15","msg":"hello, go module","uri":"/foo/bar"}

这下,我们的“ hellomodule”程序可算创建成功了。我们也看到使用 Go Module 的构建模式,go build 完全可以承担其构建规模较大、依赖复杂的 Go 项目的重任。

问题一:依赖的包下载到哪里了?还在GOPATH/src里吗?

不在。 使用Go的包管理方式,依赖的第三方包被下载到了$GOPATH/pkg/mod路径下。

问题二: 依赖包的版本是怎么控制的?

在上一个问题里,可以看到最终下载在$GOPATH/pkg/mod 下的包中最后会有一个版本号 v1.0.5,也就是说,$GOPATH/pkg/mod里可以保存相同包的不同版本。

版本是在go.mod中指定的。如果,在go.mod中没有指定,go命令会自动下载代码中的依赖的最新版本,本例就是自动下载最新的版本。如果,在go.mod用require语句指定包和版本 ,go命令会根据指定的路径和版本下载包, 指定版本时可以用latest,这样它会自动下载指定包的最新版本;

问题三: 可以把项目放在$GOPATH/src下吗?

可以。但是go会根据GO111MODULE的值而采取不同的处理方式,默认情况下,GO111MODULE=auto 自动模式

1.auto 自动模式下,项目在$GOPATH/src里会使用$GOPATH/src的依赖包,在$GOPATH/src外,就使用go.mod 里 require的包

2.on 开启模式,1.12后,无论在$GOPATH/src里还是在外面,都会使用go.mod 里 require的包

3.off 关闭模式,就是老规矩。

问题四: 依赖包中的地址失效了怎么办?比如 golang.org/x/… 下的包都无法下载怎么办?

在go快速发展的过程中,有一些依赖包地址变更了。以前的做法:

1.修改源码,用新路径替换import的地址

2.git clone 或 go get 新包后,copy到$GOPATH/src里旧的路径下

无论什么方法,都不便于维护,特别是多人协同开发时。

使用go.mod就简单了,在go.mod文件里用 replace 替换包,例如

replace golang.org/x/text => github.com/golang/text latest

这样,go会用 github.com/golang/text 替代golang.org/x/text,原理就是下载github.com/golang/text 的最新版本到 $GOPATH/pkg/mod/golang.org/x/text下。

问题五: init生成的go.mod的模块名称有什么用?

本例里,用 go mod init hello 生成的go.mod文件里的第一行会申明module hello

因为我们的项目已经不在$GOPATH/src里了,那么引用自己怎么办?就用模块名+路径

例如,在项目下新建目录 utils,创建一个tools.go文件:

在根目录下的hello.go文件就可以 import “hello/utils” 引用utils

1
import hello/utils