企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持知识库和私有化部署方案 广告
## Go Modules有哪些特点: * Go Modules是官方正式推出的包依赖管理项目 * Go modules 出现的目的之一就是为了解决 GOPATH 的问题,也就相当于是抛弃 GOPATH 了 以前项目必须在`$GOPATH/src`里进行,现在Go 允许在`$GOPATH/src`外的任何目录下使用 go.mod 创建项目 * 随着模块一起推出的还有模块代理协议(Module proxy protocol),通过这个协议我们可以实现 Go 模块代理(Go module proxy),也就是依赖镜像 * Global Caching 这个主要是针对 Go modules 的全局缓存数据说明,如下: * 同一个模块版本的数据只缓存一份,所有其他模块共享使用。 * 目前所有模块版本数据均缓存在`$GOPATH/pkg/mod`和`$GOPATH/pkg/sum`下,未来或将移至`$GOCACHE/mod`和`$GOCACHE/sum`下( 可能会在当`$GOPATH`被淘汰后)。 * 可以使用`go clean -modcache`清理所有已缓存的模块版本数据。 另外在 Go1.11 之后 GOCACHE 已经不允许设置为 off 了 如果你的版本是go1.12或更早版本,这里建议升级到go1.13,来体验一把go modules ## Go Modules相关操作 配置环境变量 ~~~ #修改 GOBIN 路径(可选) go env -w GOBIN=$HOME/bin #打开 Go modules go env -w GO111MODULE=on #设置 GOPROXY go env -w GOPROXY=https://goproxy.cn,direct ~~~ go env -w: Go1.13 新增了`go env -w`用于写入环境变量,而写入的地方是`os.UserConfigDir`所返回的路径,需要注意的是`go env -w`不会覆写。需要指出,它不会覆盖系统环境变量。 GO111MODULE: 这个环境变量主要是 Go modules 的开关,主要有以下参数: * auto:只在项目包含了 go.mod 文件时启用 Go modules,在 Go 1.13 中仍然是默认值,详见 :golang.org/issue/31857。 * on:无脑启用 Go modules,推荐设置,未来版本中的默认值,让 GOPATH 从此成为历史。 * off:禁用 Go modules。 GOPROXY: 这个环境变量主要是用于设置 Go 模块代理,它的值是一个以英文逗号 “,” 分割的 Go module proxy 列表,默认是proxy.golang.org,国内访问不了。这里要感谢盛傲飞和七牛云为中国乃至全世界的 Go 语言开发者提供免费、可靠的、持续在线的且经过CDN加速Go module proxy(goproxy.cn)。 其实值列表中的 “direct” 为特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),当值列表中上一个 Go module proxy 返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,遇见 EOF 时终止并抛出类似 “invalid version: unknown revision…” 的错误。 ### 3、创建你的项目 这里我们在`$GOPATH/src`外,创建 /var/www/demo实例 ~~~ mkdir /var/www/demo cd /var/www/demo ~~~ 新建main.go ~~~ package main import ( "github.com/gin-gonic/gin" "fmt" ) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { fmt.Println("hello world!") c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 } ~~~ ### 4、在/var/www/demo根目录下 ~~~ #生成go.mod文件 go mod init demo ~~~ 打开go.mod文件,内容 ~~~ module demo go 1.13 ~~~ go.mod 是启用了 Go moduels 的项目所必须的最重要的文件,它描述了当前项目(也就是当前模块)的元信息,每一行都以一个动词开头,目前有以下 5 个动词: * module:用于定义当前项目的模块路径。 * go:用于设置预期的 Go 版本。 * require:用于设置一个特定的模块版本。 * exclude:用于从使用中排除一个特定的模块版本。 * replace:用于将一个模块版本替换为另外一个模块版本。 这里的填写格式基本为包引用路径+版本号,另外比较特殊的是`go $version`,目前从 Go1.13 的代码里来看,还只是个标识作用,暂时未知未来是否有更大的作用。 ### 5、在/var/www/demo根目录下,执行 go build ~~~ go build ~~~ 完成后项目 ~~~ ├── demo ├── go.mod ├── go.sum └── main.go ~~~ 项目中增加了go.sum、demo文件 go.sum文件内容 ~~~ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= ... ~~~ go.sum类似于比如 dep 的 Gopkg.lock 的一类文件,它详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go在今后的操作中保证项目所依赖的那些模块版本不会被篡改。 我们可以看到一个模块路径可能有如下两种: ~~~ github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= ~~~ 前者为 Go modules 打包整个模块包文件 zip 后再进行 hash 值,而后者为针对 go.mod 的 hash 值。他们两者,要不就是同时存在,要不就是只存在 go.mod hash。 那什么情况下会不存在 zip hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 zip hash,就会出现不存在 zip hash,只存在 go.mod hash 的情况。 go.mod文件内容发生了变化,增加了 ~~~ require github.com/gin-gonic/gin v1.4.0 ~~~ 默认使用最新版本的package。 ## 更换依赖版本 查看gin所有历史版本 ~~~ 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.3.0,怎么办? 只需执行如下命令 ~~~ go mod edit -require="github.com/gin-gonic/gin@v1.3.0" go mod tidy #更新现有依赖 ~~~ @后跟版本号,这个时候go.mod已经修改好了 ~~~ require github.com/gin-gonic/gin v1.3.0 ~~~ 查看所有项目依赖的包 ~~~ go list -m all ~~~ ~~~ github.com/davecgh/go-spew v1.1.0 github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 github.com/gin-gonic/gin v1.4.0 github.com/golang/protobuf v1.3.1 github.com/json-iterator/go v1.1.6 github.com/mattn/go-isatty v0.0.7 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd github.com/modern-go/reflect2 v1.0.1 github.com/pmezard/go-difflib v1.0.0 github.com/stretchr/objx v0.1.0 github.com/stretchr/testify v1.3.0 github.com/ugorji/go v1.1.4 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 golang.org/x/text v0.3.0 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/go-playground/validator.v8 v8.18.2 gopkg.in/yaml.v2 v2.2.2 ~~~ ##快速迁移项目至 Go Modules 1. 在你项目的根目录下执行 go mod init 项目名 (项目名可不加),以生成 go.mod 文件。 2. 执行 go mod tidy` 更新整理现有的依赖,删除未使用的依赖。 ### 注意 使用go.mod管理依赖会对go get命令产生一定影响, * 用`go help module-get`和`go help gopath-get`分别去了解 Go modules 启用和未启用两种状态下的 go get 的行为 * 用`go get`拉取新的依赖 * 拉取最新的版本(优先择取 tag):`go get golang.org/x/text@latest` * 拉取`master`分支的最新 commit:`go get golang.org/x/text@master` * 拉取 tag 为 v0.3.2 的 commit:`go get golang.org/x/text@v0.3.2` * 拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2:`go get golang.org/x/text@342b2e` * 用`go get -u`更新现有的依赖 ## go mod 相关命令 `go mod download`下载 go.mod 文件中指明的所有依赖 `go mod tidy`整理现有的依赖,删除未使用的依赖。 `go mod graph`查看现有的依赖结构 `go mod init`生成 go.mod 文件 (Go 1.13 中唯一一个可以生成 go.mod 文件的子命令) `go mod edit`编辑 go.mod 文件 `go mod vendor`导出现有的所有依赖 (事实上 Go modules 正在淡化 Vendor 的概念) `go mod verify`校验一个模块是否被篡改过 `go clean -modcache`清理所有已缓存的模块版本数据。 `go mod`查看所有 go mod的使用命令。 为了更进一步的讲解,我们模拟引用如下: ``` module github.com/eddycjy/module\-repo go 1.13 require ( example.com/apple v0.1.2 example.com/banana v1.2.3 example.com/banana/v2 v2.3.4 example.com/pear // indirect example.com/strawberry // incompatible ) exclude example.com/banana v1.2.4 replace example.com/apple v0.1.2 => example.com/fried v0.1.0 ``` * module:用于定义当前项目的模块路径 * go:用于标识当前模块的 Go 语言版本,值为初始化模块时的版本,目前来看还只是个标识作用。 * require:用于设置一个特定的模块版本。 * exclude:用于从使用中排除一个特定的模块版本。 * replace:用于将一个模块版本替换为另外一个模块版本。 另外你会发现`example.com/pear`的后面会有一个 indirect 标识,indirect 标识表示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动`go get`拉取下来的,也有可能是你所依赖的模块所依赖的,情况有好几种。 ## 查看全局缓存 我们刚刚成功的将`github.com/eddycjy/mquote`模块拉取了下来,其拉取的结果缓存在  `$GOPATH/pkg/mod`和`$GOPATH/pkg/sumdb`目录下,而在`mod`目录下会以`github.com/foo/bar`的格式进行存放,如下: ~~~ mod ├── cache ├── github.com ├── golang.org ├── google.golang.org ~~~ 需要注意的是同一个模块版本的数据只缓存一份,所有其它模块共享使用。如果你希望清理所有已缓存的模块版本数据,可以执行`go clean -modcache`命令。 ## Go Modules 下的 go get 行为 在拉取项目依赖时,你会发现拉取的过程总共分为了三大步,分别是 finding(发现)、downloading(下载)以及 extracting(提取), 并且在拉取信息上一共分为了三段内容: ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X2pwZy8zM0s5Q2FrM0ZCM0N0M0F4OGdTTDBubnFZT0huY0lTTVE2VndlRmQ3ZkxmRmliaWNtelZ4OW5ORWV3cHVpYWVUV2t4Y0VhV2dzckdsQVZiMm5hY2FUd2ljc3cvNjQw?x-oss-process=image/format,png) 需要注意的是,所拉取版本的 commit 时间是以UTC时区为准,而并非本地时区,同时我们会发现我们`go get`命令所拉取到的版本是 v0.0.0,这是因为我们是直接执行`go get -u`获取的,并没有指定任何的版本信息,由 Go modules 自行按照内部规则进行选择。 ### go get 的拉取行为 刚刚我们用`go get`命令拉取了新的依赖,那么`go get`又提供了哪些功能呢,常用的拉取命令如下: | 命令 | 作用 | | --- | --- | | go get | 拉取依赖,会进行指定性拉取(更新),并不会更新所依赖的其它模块。 | | go get -u | 更新现有的依赖,会强制更新它所依赖的其它全部模块,不包括自身。 | | go get -u -t ./... | 更新所有直接依赖和间接依赖的模块版本,包括单元测试中用到的。 | 那么我想选择具体版本应当如何执行呢,如下: | 命令 | 作用 | | --- | --- | | go get golang.org/x/text@latest | 拉取最新的版本,若存在tag,则优先使用。 | | go get golang.org/x/text@master | 拉取 master 分支的最新 commit。 | | go get golang.org/x/text@v0.3.2 | 拉取 tag 为 v0.3.2 的 commit。 | | go get golang.org/x/text@342b2e | 拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2。 | ### go get 的版本选择 我们回顾一下我们拉取的`go get github.com/eddycjy/mquote`,其结果是`v0.0.0-20200220041913-e066a990ce6f`,对照着上面所提到的`go get`行为来看,你可能还会有一些疑惑,那就是在`go get`没有指定任何版本的情况下,它的版本选择规则是怎么样的,也就是为什么`go get`拉取的是`v0.0.0`,它什么时候会拉取正常带版本号的 tags 呢。实际上这需要区分两种情况,如下: 1. 所拉取的模块有发布 tags: * 如果只有单个模块,那么就取主版本号最大的那个tag。 * 如果有多个模块,则推算相应的模块路径,取主版本号最大的那个tag(子模块的tag的模块路径会有前缀要求) * 所拉取的模块没有发布过 tags: * 默认取主分支最新一次 commit 的 commithash。 没有发布过 tags 那么为什么会拉取的是`v0.0.0`呢,是因为`github.com/eddycjy/mquote`没有发布任何的tag,如下: ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X2pwZy8zM0s5Q2FrM0ZCM0N0M0F4OGdTTDBubnFZT0huY0lTTWljbDJINUR2eE5CWlduTzFpYWRYMGczSWRtTFRCM1NLZ3haZ0hKNlBneU14cGpiTnkxaWNReHg1QS82NDA?x-oss-process=image/format,png) 因此它默认取的是主分支最新一次 commit 的 commit 时间和 commithash,也就是`20200220041913-e066a990ce6f`,属于第二种情况。 ## 有发布 tags 在项目有发布 tags 的情况下,还存在着多种模式,也就是只有单个模块和多个模块,我们统一以多个模块来进行展示,因为多个模块的情况下就已经包含了单个模块的使用了,如下图: ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X2pwZy8zM0s5Q2FrM0ZCM0N0M0F4OGdTTDBubnFZT0huY0lTTVVVR1g2Z2JzRmZSYUxNYlFCTlVkeVZmNURpYXlVRWg4a25CWXNpYVVnQ2hYekcycjlyQ0hydVJnLzY0MA?x-oss-process=image/format,png) 在这个项目中,我们一共打了两个tag,分别是:v0.0.1 和 module/tour/v0.0.1。这时候你可能会奇怪,为什么要打`module/tour/v0.0.1`这么“奇怪”的tag,这有什么用意吗? 其实是 Go modules 在同一个项目下多个模块的tag表现方式,其主要目录结构为: ~~~ mquote ├── go.mod ├── module │   └── tour │   ├── go.mod │   └── tour.go ~~~ 可以看到在`mquote`这个项目的根目录有一个 go.mod 文件,而在`module/tour`目录下也有一个 go.mod 文件,其模块导入和版本信息的对应关系如下: | tag | 模块导入路径 | 含义 | | --- | --- | --- | | v0.0.1 | github.com/eddycjy/mquote | mquote 项目的v 0.0.1 版本 | | module/tour/v0.01 | github.com/eddycjy/mquote/module/tour | mquote 项目下的子模块 module/tour 的 v0.0.1 版本 | ## 导入主模块和子模块 结合上述内容,拉取主模块的话,还是照旧执行如下命令: ~~~ $ go get github.com/eddycjy/mquote@v0.0.1 go: finding github.com/eddycjy/mquote v0.0.1 go: downloading github.com/eddycjy/mquote v0.0.1 go: extracting github.com/eddycjy/mquote v0.0.1 ~~~ 如果是想拉取子模块,执行如下命令: ~~~ $ go get github.com/eddycjy/mquote/module/tour@v0.0.1 go: finding github.com/eddycjy/mquote/module v0.0.1 go: finding github.com/eddycjy/mquote/module/tour v0.0.1 go: downloading github.com/eddycjy/mquote/module/tour v0.0.1 go: extracting github.com/eddycjy/mquote/module/tour v0.0.1 ~~~ 我们将主模块和子模块的拉取进行对比,你会发现子模块的拉取会多出一步,它会先发现`github.com/eddycjy/mquote/module`,再继续推算,最终拉取到`module/tour`。 ## Go Modules 的导入路径说明 ### 不同版本的导入路径 在前面的模块拉取和引用中,你会发现我们的模块导入路径就是`github.com/eddycjy/mquote`和  `github.com/eddycjy/mquote/module/tour`,似乎并没有什么特殊的。 其实不然,实际上 Go modules 在主版本号为 v0 和 v1 的情况下省略了版本号,而在主版本号为v2及以上则需要明确指定出主版本号,否则会出现冲突,其tag与模块导入路径的大致对应关系如下: | tag | 模块导入路径 | | --- | --- | | v0.0.0 | github.com/eddycjy/mquote | | v1.0.0 | github.com/eddycjy/mquote | | v2.0.0 | github.com/eddycjy/mquote/v2 | | v3.0.0 | github.com/eddycjy/mquote/v3 | 简单来讲,就是主版本号为 v0 和 v1 时,不需要在模块导入路径包含主版本的信息,而在 v1 版本以后,也就是 v2 起,必须要在模块的导入路径末尾加上主版本号,引用时就需要调整为如下格式: ~~~ import ( "github.com/eddycjy/mquote/v2/example") ~~~ 另外忽略主版本号 v0 和 v1 是强制性的(不是可选项),因此每个软件包只有一个明确且规范的导入路径。 ### 为什么忽略 v0 和 v1 的主版本号 1. 导入路径中忽略 v1 版本的原因是:考虑到许多开发人员创建一旦到达 v1 版本便永不改变的软件包,这是官方所鼓励的,不认为所有这些开发人员在无意发布 v2 版时都应被迫拥有明确的 v1 版本尾缀,这将导致 v1 版本变成“噪音”且无意义。 2. 导入路径中忽略了 v0 版本的原因是:根据语义化版本规范,v0的这些版本完全没有兼容性保证。需要一个显式的 v0 版本的标识对确保兼容性没有多大帮助。 ## Go Modules 的语义化版本控制 我们不断地在 Go Modules 的使用中提到版本号,其实质上被称为“语义化版本”,假设我们的版本号是 v1.2.3,如下: ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X2pwZy8zM0s5Q2FrM0ZCM0N0M0F4OGdTTDBubnFZT0huY0lTTWtiZlk2UjRTMGhoSENtMUpOODdDZHN5WWliQ1ZBMG45ZTRuN1NsaWI4VFVWWE52Yll3dzlnNGRRLzY0MA?x-oss-process=image/format,png) 其版本格式为 “主版本号.次版本号.修订号”,版本号的递增规则如下: 1. 主版本号:当你做了不兼容的 API 修改。 2. 次版本号:当你做了向下兼容的功能性新增。 3. 修订号:当你做了向下兼容的问题修正。 假设你是先行版本号或特殊情况,可以将版本信息追加到“主版本号.次版本号.修订号”的后面,作为延伸,如下: ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X2pwZy8zM0s5Q2FrM0ZCM0N0M0F4OGdTTDBubnFZT0huY0lTTTlVd2dIVmZHcElFMk9sMzB5a21YVGliWEtXTVBzcTRlR3N5SXU1cWNta05zZlM5ZktZWUVvVUEvNjQw?x-oss-process=image/format,png) 至此我们介绍了 Go modules 所支持的两类版本号方式,在我们发布新版本打 tag 的时候,需要注意遵循,否则不遵循语义化版本规则的版本号都是无法进行拉取的。 ## Go Modules 的最小版本选择 现在我们已经有一个模块,也有发布的 tag,但是一个模块往往依赖着许多其它许许多多的模块,并且不同的模块在依赖时很有可能会出现依赖同一个模块的不同版本,如下图(来自 Russ Cox): ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X2pwZy8zM0s5Q2FrM0ZCM0N0M0F4OGdTTDBubnFZT0huY0lTTTlVd2dIVmZHcElFMk9sMzB5a21YVGliWEtXTVBzcTRlR3N5SXU1cWNta05zZlM5ZktZWUVvVUEvNjQw?x-oss-process=image/format,png) 在上述依赖中,模块 A 依赖了模块 B 和模块 C,而模块 B 依赖了模块 D,模块 C 依赖了模块 D 和 F,模块 D 又依赖了模块 E,而且同模块的不同版本还依赖了对应模块的不同版本。那么这个时候 Go modules 怎么选择版本,选择的是哪一个版本呢? 我们根据 proposal 可得知,Go modules 会把每个模块的依赖版本清单都整理出来,最终得到一个构建清单,如下图(来自 Russ Cox): ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X2pwZy8zM0s5Q2FrM0ZCM0N0M0F4OGdTTDBubnFZT0huY0lTTVhBUFh0Rlo1ZDRsOFRWaWNyckR4R01oTTM4aEJIY2liNHhpYWtISVZZVmcwQjFlUXU2OUNMRVlmdy82NDA?x-oss-process=image/format,png) 我们看到 rough list 和 final list,两者的区别在于重复引用的模块 D(v1.3、v1.4),其最终清单选用了模块 D 的 v1.4 版本,主要原因: 1. 语义化版本的控制:因为模块 D 的 v1.3 和 v1.4 版本变更,都属于次版本号的变更,而在语义化版本的约束下,v1.4 必须是要向下兼容 v1.3 版本,因此认为不存在破坏性变更,也就是兼容的。 2. 模块导入路径的规范:主版本号不同,模块的导入路径不一样,因此若出现不兼容的情况,其主版本号会改变,模块的导入路径自然也就改变了,因此不会与第一点的基础相冲突。 ## go.sum 文件要不要提交 理论上 go.mod 和 go.sum 文件都应该提交到你的 Git 仓库中去。 假设我们不上传 go.sum 文件,就会造成每个人执行 Go modules 相关命令,又会生成新的一份 go.sum,也就是会重新到上游拉取,再拉取时有可能就是被篡改过的了,会有很大的安全隐患,失去了与基准版本(第一个所提交的人,所期望的版本)的校验内容,因此 go.sum文件是需要提交。 ## 总结 至此我们介绍了 Go modules 的前世今生、基本使用和在 Go modules 模式下`go get`命令的行为转换,同时我们对常见的多版本导入路径、语义化版本控制以及多模块的最小版本选择规则进行了大致的介绍。