在 Go 1.5 版本之前,所有的依赖包都是存放在 GOPATH 下,没有版本控制。这种方式的最大的弊端就是无法实现包的多版本控制,比如项目 A 和项目 B 依赖于不同版本的 package,如果 package 没有做到完全的向前兼容,往往会导致一些问题。
除此之外还会有下面的问题:
- 如果依赖包持续演进,可能会导致不同开发者在不同时间获取和编译同一个 Go 包时,得到不同的结果,也就是不能保证可重现的构建。
- 如果依赖包引入了不兼容代码,程序将无法通过编译。
- 如果依赖包因引入新代码而无法正常通过编译,并且该依赖包的作者又没用及时修复这个问题,这种错误也会传导到你的程序,导致你的程序无法通过编译。
Go 1.5 版本推出了 vendor 机制,所谓 vendor 机制,就是每个项目的根目录下可以有一个 vendor 目录,里面存放了该项目的依赖的 package。go build 的时候会先去 vendor 目录查找依赖,如果没有找到会再去 GOPATH 目录下查找。
Go 编译器会优先感知和使用 vendor 目录下缓存的第三方包版本,而不是 GOPATH 环境变量所配置的路径下的第三方包版本。这样,无论第三方依赖包自己如何变化,无论 GOPATH 环境变量所配置的路径下的第三方包是否存在、版本是什么,都不会影响到 Go 程序的构建。
如果你将 vendor 目录和项目源码一样提交到代码仓库,那么其他开发者下载你的项目后,就可以实现可重现的构建。因此,如果使用 vendor 机制管理第三方依赖包,最佳实践就是将 vendor 一并提交到代码仓库中。
要想开启 vendor 机制,你的 Go 项目必须位于 GOPATH 环境变量配置的某个路径的 src 目录下面。如果不满足这一路径要求,那么 Go 编译器是不会理会 Go 项目目录下的 vendor 目录的。
不过 vendor 机制虽然一定程度解决了 Go 程序可重现构建的问题,但对开发者来说,它的体验却不那么好。一方面,Go 项目必须放在 GOPATH 环境变量配置的路径下,庞大的 vendor 目录需要提交到代码仓库,不仅占用代码仓库空间,减慢仓库下载和更新的速度,而且还会干扰代码评审,对实施代码统计等开发者效能工具也有比较大影响。另外,你还需要手工管理 vendor 下面的 Go 依赖包,包括项目依赖包的分析、版本的记录、依赖包获取和存放等等。
Go 1.9 版本推出了实验性质的包管理工具 dep,这里把 dep 归结为 Golang 官方的包管理方式可能有一些不太准确。关于 dep 的争议颇多。
1.3 go modGo 1.11 版本推出 modules 机制,简称 mod,更加易于管理项目中所需要的模块。模块是存储在文件树中的 Go 包的集合,其根目录中包含 go.mod 文件。 go.mod 文件定义了模块的模块路径,它也是用于根目录的导入路径,以及它的依赖性要求。每个依赖性要求都被写为模块路径和特定语义版本。
从 Go 1.11 开始,Go 允许在 $GOPATH/src 外的任何目录下使用 go.mod 创建项目。在 $GOPATH/src 中,为了兼容性,Go 命令仍然在旧的 GOPATH 模式下运行。从 Go 1.13 开始,go.mod模式将成为默认模式。
Go Modules 在 Go 1.11 及 Go 1.12 中有三个模式,根据环境变量 GO111MODULE 定义:
- 默认模式(未设置该环境变量或 GO111MODULE=auto)
Go 命令行工具在同时满足以下两个条件时使用 Go Modules
- 当前目录不在 GOPATH/src/ 下;
- 在当前目录或上层目录中存在 go.mod 文件;
- GOPATH 模式(GO111MODULE=off)
Go 命令行工具从不使用 Go Modules。相反,它查找 vendor 目录和 GOPATH 以查找依赖项。
- Go Modules 模式( GO111MODULE=on)
Go 命令行工具只使用 Go Modules,GOPATH不再作为导入目录,但它还是会把下载的依赖储存在 GOPATH/pkg/mod 中,也会把 goinstall的结果放在 GOPATH/bin 中,只移除了 GOPATH/src/。
Go 1.13 默认使用 Go Modules 模式,所以以上内容在 Go 1.13 发布并在生产环境中使用后都可以忽略。
本文以 Go 1.13.6 为基础详细说明 Go modules 的使用。
# 临时开启 Go modules 功能 export GO111MODULE=on # 永久开启 Go modules 功能 go env -w GO111MODULE=on # 设置 Go 的国内代理,方便下载第三方包 go env -w GOPROXY=https://goproxy.cn,direct
逗号后面可以增加多个 proxy,最后的 direct 则是在所有 proxy 都找不到的时候,直接访问,代理访问不到的私有仓库就可以正常使用了。
其它代理请参考:
https://www.cnblogs.com/feiquan/p/13357971.html
https://studygolang.com/articles/23599?fr=sidebar
设置后通过 go env 查看如下所示:
wohu@wohu-dev:~$ go env GO111MODULE="on" ..... GOPROXY="https://goproxy.cn,direct" ..... wohu@wohu-dev:~$3. Go module 常用操作 3.1 初始化项目
我们在 $GOPATH 以外的目录创建一个任意目录,然后初始化 go mod init project_name,成功之后会发现目录下会生成一个 go.mod 文件。
wohu@wohu-dev:~$ mkdir goProject wohu@wohu-dev:~$ cd goProject/ wohu@wohu-dev:~/goProject$ mkdir apiserver wohu@wohu-dev:~/goProject$ cd apiserver/ wohu@wohu-dev:~/goProject/apiserver$ go mod init apiserver go: creating new go.mod: module apiserver wohu@wohu-dev:~/goProject/apiserver$ ls go.mod wohu@wohu-dev:~/goProject/apiserver$
查看内容
wohu@wohu-dev:~/goProject/apiserver$ cat go.mod module apiserver go 1.13 wohu@wohu-dev:~/goProject/apiserver$
go.mod 文件只存在于模块的根目录中。模块子目录的代码包的导入路径等于模块根目录的导入路径(就是前面说的 module path)加上子目录的相对路径。
比如,我们如果创建了一个子目录叫 common,我们不需要(也不会想要)在子目录里面再运行一次 go mod init了,这个代码包会被认为就是 apiserver 模块的一部分,而这个代码包的导入路径就是 apiserver/common。
在 apiserver 文件夹下创建 main.go 并添加以下内容
package main
import "github.com/gin-gonic/gin"
func ping(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
}
func main() {
r := gin.Default()
r.GET("/ping", ping)
r.Run() // listen and serve on 0.0.0.0:8080
}
执行 go build main.go 之后会自动下载三方包到默认的目录 $GOPATH/pkg/mod,也就是 Mod Cache 路径。
wohu@wohu-dev:~/goProject/apiserver$ go build main.go go: finding github.com/gin-gonic/gin v1.7.6 go: downloading github.com/gin-gonic/gin v1.7.6 ... wohu@wohu-dev:~/goProject/apiserver$ ls go.mod go.sum main main.go wohu@wohu-dev:~/goProject/apiserver$
进入 $GOPATH/pkg/mod 目录查看
wohu@wohu-dev:~/goProject/apiserver$ ls $GOPATH/pkg/mod/cache/download/github.com/gin-gonic/gin/@v/ list v1.7.4.mod v1.7.5.lock v1.7.6.info v1.7.6.ziphash list.lock v1.7.4.zip v1.7.5.mod v1.7.6.lock v1.7.4.info v1.7.4.ziphash v1.7.5.zip v1.7.6.mod v1.7.4.lock v1.7.5.info v1.7.5.ziphash v1.7.6.zip wohu@wohu-dev:~/goProject/apiserver$
此时再次查看 go.mod文件内容
module apiserver go 1.13 require github.com/gin-gonic/gin v1.7.6 // indirect
其中
- module 表示模块名称
- require 依赖包列表以及版本
一般来说,require () 是不需要自己手动去修改的,当运行代码的时候,会根据代码中用到的包自动去下载导入。
- exclude 禁止依赖包列表,不下载和引用哪些包(仅在当前模块为主模块时生效)
- replace 替换依赖包列表和引用路径(仅在当前模块为主模块时生效)
replace 对于国内开发来说是个神功能,他可以将代码中使用,但国内被墙的代码替换成 github上的下载路径,例如:golang.org/x/ 下的包,全都替换成 github地址上的包,版本使用 latest 即可
replace ( golang.org/x/net => github.com/golang/net latest golang.org/x/tools => github.com/golang/tools latest golang.org/x/crypto => github.com/golang/crypto latest golang.org/x/sys => github.com/golang/sys latest golang.org/x/text => github.com/golang/text latest golang.org/x/sync => github.com/golang/sync latest )
indirect 表示这个库是间接引用进来的。
使用 go list -m all 可以查看到所有依赖列表,也可以使用 go list -json -m all 输出 json格式的打印结果。
wohu@wohu-dev:~/goProject/apiserver$ go list -m all apiserver github.com/davecgh/go-spew v1.1.1 github.com/gin-contrib/sse v0.1.0 ... gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 gopkg.in/yaml.v2 v2.2.8 wohu@wohu-dev:~/goProject/apiserver$
除了 go.mod 之外,go 命令行工具还维护了一个 go.sum 文件,它包含了指定的模块的版本内容的哈希值作为校验参考:
wohu@wohu-dev:~/goProject/apiserver$ cat go.sum github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
go 命令行工具使用 go.sum 文件来确保你的项目依赖的模块不会发生变化——无论是恶意的,还是意外的,或者是其它的什么原因。go.mod 文件和 go.sum 文件都应该保存到你的代码版本控制系统里面去。
go.sum 这个文件记录了源码的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。在构建的时候,如果本地依赖包的 hash 值与 go.sum 文件中记录的不一致,就会被拒绝构建,这样可以确保你的项目所依赖的 module 内容,不会被恶意或意外篡改。
将 GIN 框架的版本回退到上个版本。这里需要使用一个命令查看依赖的版本历史。
wohu@wohu-dev:~/goProject/apiserver$ go list -m -versions github.com/gin-gonic/gin github.com/gin-gonic/gin v1.1.1 v1.1.2 v1.1.3 v1.1.4 v1.3.0 v1.4.0 v1.5.0 v1.6.0 v1.6.1 v1.6.2 v1.6.3 v1.7.0 v1.7.1 v1.7.2 v1.7.3 v1.7.4 v1.7.5 v1.7.6 wohu@wohu-dev:~/goProject/apiserver$
将版本回退到指定版本有两种方法:
- 使用 go get 命令
# 只需要在依赖后面加上 @version 就可以了 wohu@wohu-dev:~/goProject/apiserver$ go get github.com/gin-gonic/gin@v1.7.5 go: finding github.com/gin-gonic/gin v1.7.5 go: downloading github.com/gin-gonic/gin v1.7.5 go: extracting github.com/gin-gonic/gin v1.7.5 go: downloading github.com/json-iterator/go v1.1.9 go: extracting github.com/json-iterator/go v1.1.9 ......
请注意我们给 go get 命令的参数后面显式地指定了 @v1.7.5 ,事实上每个传递给 go get 的参数都能在后面显式地指定一个版本号,默认情况下这个版本号是 @latest,这代表 Go 命令行工具会尝试下载最新的版本。
查看回退之后的版本
wohu@wohu-dev:~/goProject/apiserver$ go list -m all apiserver github.com/gin-gonic/gin v1.7.5
- 使用 go mod 命令
wohu@wohu-dev:~/goProject/apiserver$ go mod edit -require="github.com/gin-gonic/gin@v1.7.4" wohu@wohu-dev:~/goProject/apiserver$ go get go: downloading github.com/gin-gonic/gin v1.7.4 go: extracting github.com/gin-gonic/gin v1.7.4 go: finding github.com/gin-gonic/gin v1.7.4 wohu@wohu-dev:~/goProject/apiserver$ wohu@wohu-dev:~/goProject/apiserver$ go list -m all apiserver .... github.com/gin-gonic/gin v1.7.4
在 Go Module 构建模式下,当依赖的主版本号为 0 或 1 的时候,我们在 Go 源码中导入依赖包,不需要在包的导入路径上增加版本号,也就是:
import github.com/user/repo/v0 等价于 import github.com/user/repo import github.com/user/repo/v1 等价于 import github.com/user/repo3.4 升级版本
升级版本和回退版本使用的命令一样,只是后面的版本号不同,不再额外说明
3.5 删除未使用的依赖项我们在构建一个代码包的时候(比如说 go build 或者 go test),可以轻易的知道哪些依赖缺失,从而将它自动添加进来,但很难知道哪些依赖可以被安全的移除掉。移除一个依赖项需要在检查完模块中所有代码包和这些代码包的所有可能的编译标签的组合。一个普通的 build 命令不会获得这么多的信息,所以它不能保证安全地移除掉没用的依赖项。
可以用 go mod tidy 命令来清除这些没用到的依赖项:
go mod tidy
查看 go mod 命令选项
wohu@wohu-dev:~/goProject/apiserver$ go mod Go mod provides access to operations on modules. Note that support for modules is built into all the go commands, not just 'go mod'. For example, day-to-day adding, removing, upgrading, and downgrading of dependencies should be done using 'go get'. See 'go help modules' for an overview of module functionality. Usage: go mod[arguments] The commands are: download download modules to local cache # 下载依赖的module到本地cache edit edit go.mod from tools or scripts # 编辑go.mod文件 graph print module requirement graph # 打印模块依赖图 init initialize new module in current directory # 在当前文件夹下初始化一个新的module, 创建go.mod文件 tidy add missing and remove unused modules # 增加丢失的module,去掉未用的module vendor make vendored copy of dependencies # 将依赖复制到vendor下 verify verify dependencies have expected content # 校验依赖 why explain why packages or modules are needed # 解释为什么需要依赖 Use "go help mod " for more information about a command.
go mod tidy 命令会扫描 Go 源码,并自动找出项目依赖的外部 Go Module 以及版本,下载这些依赖并更新本地的 go.mod 文件。
由 go mod tidy 下载的依赖 module 会被放置在本地的 module 缓存路径下,默认值为 $GOPATH/pkg/mod,Go 1.15 及以后版本可以通过 GOMODCACHE 环境变量,自定义本地 module 的缓存路径。
3.6 引入主版本号大于 1 的三方库语义导入版本机制有一个原则:如果新旧版本的包使用相同的导入路径,那么新包与旧包是兼容的。也就是说,如果新旧两个包不兼容,那么我们就应该采用不同的导入路径。
按照语义版本规范,如果我们要为项目引入主版本号大于 1 的依赖,比如 v2.0.0,那么由于这个版本与 v1、v0 开头的包版本都不兼容,我们在导入 v2.0.0包时,就要使用像下面代码中那样不同的包导入路径:
import github.com/user/repo/v2/xxx
也就是说,如果我们要为 Go 项目添加主版本号大于 1 的依赖,我们就需要使用“语义导入版本”机制,在声明它的导入路径的基础上,加上版本号信息。首先,我们在源码中,以空导入的方式导入 v7 版本的 github.com/go-redis/redis 包:
package main
import (
_ "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 获取 redis 的 v7 版本:
$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.13.7 特殊情况:使用 vendor
vendor 机制虽然诞生于 GOPATH 构建模式主导的年代,但在 Go Module 构建模式下,它依旧被保留了下来,并且成为了 Go Module 构建机制的一个很好的补充。特别是在一些不方便访问外部网络,并且对 Go 应用构建性能敏感的环境。
和 GOPATH 构建模式不同,Go Module 构建模式下,我们再也无需手动维护 vendor 目录下的依赖包了,Go 提供了可以快速建立和更新 vendor 的命令,我们还是以前面的 module-mode 项目为例,通过下面命令为该项目建立 vendor:
$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 (非vendor) 模式即可满足大部分需求。如果是那种开发环境受限,因无法访问外部代理而无法通过 go 命令自动解决依赖和下载依赖的环境下,我们通过 vendor 来辅助解决。
4. go mod 工作机制 4.1 go mod 版本表达方式go mod 有两种版本表达方式,分别为语义化版本和基于某一个 commit 的伪版本。
注意:
- go get 默认不会将语义化版本修改为伪版本
- @latest 表示最新的语义化版本,而不是伪版本
- -u 表示更新该依赖并同时更新该依赖中所有参与编译的依赖到 minor 版本
下面问题的答案选择 B
myproject 有两个直接依赖 A 和 B,A 和 B 有一个共同的依赖包 C,但 A 依赖 C 的 v1.1.0 版本,而 B 依赖的是 C 的 v1.3.0 版本,并且此时 C 包的最新发布版为 v1.7.0。这个时候,Go 命令是如何为 myproject 选出间接依赖包 C 的版本呢?选出的究竟是 v1.7.0、v1.1.0 还是 v1.3.0 呢?
Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。这个例子中,C v1.3.0 是符合项目整体要求的版本集合中的版本最小的那个,于是 Go 命令选择了 C v1.3.0,而不是最新最大的 C v1.7.0。
如果现在 B 依赖的不是 v1.3.0 而是 v2.3.0 , 那最终 go 会选择 C v1.1.0 和 C v2.3.0,这两个都会下载到本地并链接到最终程序中。因为根据 go 的语义导入版本的规则,不同 major 号的 module 就是不同的 module 。
go module 采用的语义版本导入机制,v1.0.0 和 v2.0.0 的 major 号不同,是两个完全不同的版本,是可以共同被某个包同时导入的,语法如下;
import (
"c"
"c/v2"
)
5. 常见问题
5.1 导入本地创建的 module
如何 import 自己在本地创建的 module ,在这个 module 还没有发布到 GitHub 的情况下?
假如你的 module a 要 import 的 module b 将发布到 github.com/user/repo 中,那么你可以手动在module a 的 go.mod 中的 require 块中手工加上一条:
require github.com/user/repo v1.0.0
注意 v1.0.0 这个版本号是一个临时的版本号,目的是满足 go.mod 中 require 块的语法要求。
然后在 module a 的 go.mod 中使用 replace 将上面对 module b 的 require 替换为本地的 module b :
replace github.com/user/repo v1.0.0 => module b本地路径
这样 go 命令就会使用你本地正在开发、尚未提交 github 的 module b 了。
常用工具和方法
- 在使用 go get 更新时,如果不需要更新依赖的依赖则不要使用 -u 参数,如果使用该参数会使得依赖的依赖更新,有可能导致由于依赖的依赖版本不兼容而编译失败。
- 有些工程不使用 go mod 原因有两个: 一是在 go mod 推广前其版本号已经大于 1;二是便于使用 go get 直接拉取到 V2 以上的版本。
参考:
https://www.njphper.com/posts/8b58ea6d.html
https://blog.golang.org/using-go-modules
https://blog.csdn.net/alisystemsoftware/article/details/104299918
https://pkg.go.dev/cmd/go/internal/modget@go1.15.11
https://juejin.cn/video/7031152677996363779/section/7031152678222856195



