特别放送 Go Modules依赖包管理全讲 你好,我是孔令飞。今天我们更新一期特别放送作为加餐。
在Go项目开发中,依赖包管理是一个非常重要的内容,依赖包处理不好,就会导致编译失败。而且Go的依赖包管理有一定的复杂度,所以,我们有必要系统学习下Go的依赖包管理工具。
这一讲,我会首先介绍下Go依赖包管理工具的历史,并详细介绍下目前官方推荐的依赖包管理方案Go Modules。Go Modules主要包括了
go mod 命令行工具、模块下载机制,以及两个核心文件go.mod和go.sum。另外,Go Modules也提供了一些环境变量,用来控制Go Modules的行为。这一讲,我会分别介绍下这些内容。
在正式开始讲解这些内容之前,我们先来对Go Modules有个基本的了解。
Go Modules简介
Go Modules是Go官方推出的一个Go包管理方案,基于vgo演进而来,具有下面这几个特性:
- 可以使包的管理更加简单。
- 支持版本管理。
- 允许同一个模块多个版本共存。
- 可以校验依赖包的哈希值,确保包的一致性,增加安全性。
- 内置在几乎所有的go命令中,包括
go get 、
go build 、
go install 、
go run 、
go test 、
go list 等命令。
- 具有Global Caching特性,不同项目的相同模块版本,只会在服务器上缓存一份。
在Go1.14版本以及之后的版本,Go官方建议在生产环境中使用Go Modules。因此,以后的Go包管理方案会逐渐统一到Go Modules。与Go Modules相关的概念很多,我在这里把它们总结为“6-2-2-1-1”,这一讲后面还会详细介绍每个概念。
- 六个环境变量:
GO111MODULE 、
GOPROXY 、
GONOPROXY 、
GOSUMDB 、
GONOSUMDB 、
GOPRIVATE 。
- 两个概念:Go module proxy和Go checksum database。
- 两个主要文件:go.mod和go.sum。
- 一个主要管理命令:go mod。
- 一个build flag。
Go包管理的历史
在具体讲解Go Modules之前,我们先看一下Go包管理的历史。从Go推出之后,因为没有一个统一的官方方案,所以出现了很多种Go包管理方案,比较混乱,也没有彻底解决Go包管理的一些问题。Go包管理的历史如下图所示:
这张图展示了Go依赖包管理工具经历的几个发展阶段,接下来我会按时间顺序重点介绍下其中的五个阶段。
Go1.5版本前:GOPATH
在Go1.5版本之前,没有版本控制,所有的依赖包都放在GOPATH下。采用这种方式,无法实现包的多版本管理,并且包的位置只能局限在GOPATH目录下。如果A项目和B项目用到了同一个Go包的不同版本,这时候只能给每个项目设置一个GOPATH,将对应版本的包放在各自的GOPATH目录下,切换项目目录时也需要切换GOPATH。这些都增加了开发和实现的复杂度。
Go1.5版本:Vendoring
Go1.5推出了vendor机制,并在Go1.6中默认启用。在这个机制中,每个项目的根目录都可以有一个vendor目录,里面存放了该项目的Go依赖包。在编译Go源码时,Go优先从项目根目录的vendor目录查找依赖;如果没有找到,再去GOPATH下的vendor目录下找;如果还没有找到,就去GOPATH下找。
这种方式解决了多GOPATH的问题,但是随着项目依赖的增多,vendor目录会越来越大,造成整个项目仓库越来越大。在vendor机制下,一个中型项目的vendor目录有几百M的大小一点也不奇怪。
“百花齐放”:多种Go依赖包管理工具出现
这个阶段,社区也出现了很多Go依赖包管理的工具,这里我介绍三个比较有名的。
- Godep:解决包依赖的管理工具,Docker、Kubernetes、CoreOS等Go项目都曾用过godep来管理其依赖。
- Govendor:它的功能比Godep多一些,通过vendor目录下的
vendor.json 文件来记录依赖包的版本。
- Glide:相对完善的包管理工具,通过
glide.yaml 记录依赖信息,通过
glide.lock 追踪每个包的具体修改。
Govendor、Glide都是在Go支持vendor之后推出的工具,Godep在Go支持vendor之前也可以使用。Go支持vendor之后,Godep也改用了vendor模式。
Go1.9版本:Dep
对于从0构建项目的新用户来说,Glide功能足够,是个不错的选择。不过,Golang 依赖管理工具混乱的局面最终由官方来终结了:Golang官方接纳了由社区组织合作开发的Dep,作为official experiment。在相当长的一段时间里,Dep作为标准,成为了事实上的官方包管理工具。
因为Dep已经成为了official experiment的过去时,现在我们就不必再去深究了,让我们直接去了解谁才是未来的official experiment吧。
Go1.11版本之后:Go Modules
Go1.11版本推出了Go Modules机制,Go Modules基于vgo演变而来,是Golang官方的包管理工具。在Go1.13版本,Go语言将Go Modules设置为默认的Go管理工具;在Go1.14版本,Go语言官方正式推荐在生产环境使用Go Modules,并且鼓励所有用户从其他的依赖管理工具迁移过来。至此,Go终于有了一个稳定的、官方的Go包管理工具。
到这里,我介绍了Go依赖包管理工具的历史,下面再来介绍下Go Modules的使用方法。
包(package)和模块(module)
Go程序被组织到Go包中,Go包是同一目录中一起编译的Go源文件的集合。在一个源文件中定义的函数、类型、变量和常量,对于同一包中的所有其他源文件可见。
模块是存储在文件树中的Go包的集合,并且文件树根目录有go.mod文件。go.mod文件定义了模块的名称及其依赖包,每个依赖包都需要指定导入路径和语义化版本(Semantic Versioning),通过导入路径和语义化版本准确地描述一个依赖。
这里要注意,
“module” != “package” ,模块和包的关系更像是集合和元素的关系,包属于模块,一个模块是零个或者多个包的集合。下面的代码段,引用了一些包: import ( // Go 标准包 “fmt” // 第三方包 “github.com/spf13/pflag” // 匿名包 _ “github.com/jinzhu/gorm/dialects/mysql” // 内部包 “github.com/marmotedu/iam/internal/apiserver” )
这里的
fmt 、
github.com/spf13/pflag 和
github.com/marmotedu/iam/internal/apiserver 都是Go包。Go中有4种类型的包,下面我来分别介绍下。
- Go标准包:在Go源码目录下,随Go一起发布的包。
- 第三方包:第三方提供的包,比如来自于github.com的包。
- 匿名包:只导入而不使用的包。通常情况下,我们只是想使用导入包产生的副作用,即引用包级别的变量、常量、结构体、接口等,以及执行导入包的
init() 函数。
- 内部包:项目内部的包,位于项目目录下。
下面的目录定义了一个模块: $ ls hello/ go.mod go.sum hello.go hello_test.go world
hello目录下有一个go.mod文件,说明了这是一个模块,该模块包含了hello包和一个子包world。该目录中也包含了一个go.sum文件,该文件供Go命令在构建时判断依赖包是否合法。这里你先简单了解下,我会在下面讲go.sum文件的时候详细介绍。
Go Modules 命令
Go Modules的管理命令为
go mod ,
go mod 有很多子命令,你可以通过
go help mod 来获取所有的命令。下面我来具体介绍下这些命令。
- download:下载go.mod文件中记录的所有依赖包。
- edit:编辑go.mod文件。
- graph:查看现有的依赖结构。
- init:把当前目录初始化为一个新模块。
- tidy:添加丢失的模块,并移除无用的模块。默认情况下,Go不会移除go.mod文件中的无用依赖。当依赖包不再使用了,可以使用
go mod tidy 命令来清除它。
- vendor:将所有依赖包存到当前目录下的vendor目录下。
- verify:检查当前模块的依赖是否已经存储在本地下载的源代码缓存中,以及检查下载后是否有修改。
- why:查看为什么需要依赖某模块。
Go Modules开关
如果要使用Go Modules,在Go1.14中仍然需要确保Go Modules特性处在打开状态。你可以通过环境变量GO111MODULE来打开或者关闭。GO111MODULE有3个值,我来分别介绍下。
- auto:在Go1.14版本中是默认值,在
$GOPATH/src 下,且没有包含go.mod时则关闭Go Modules,其他情况下都开启Go Modules。
- on:启用Go Modules,Go1.14版本推荐打开,未来版本会设为默认值。
- off:关闭Go Modules,不推荐。
所以,如果要打开Go Modules,可以设置环境变量
export GO111MODULE=on 或者
export GO111MODULE=auto ,建议直接设置
export GO111MODULE=on 。
Go Modules使用语义化的版本号,我们开发的模块在发布版本打tag的时候,要注意遵循语义化的版本要求,不遵循语义化版本规范的版本号都是无法拉取的。
模块下载
在执行
go get 等命令时,会自动下载模块。接下来,我会介绍下go命令是如何下载模块的。主要有三种下载方式:
- 通过代理下载;
- 指定版本号下载;
- 按最小版本下载。
通过代理来下载模块
默认情况下,Go命令从VCS(Version Control System,版本控制系统)直接下载模块,例如 GitHub、Bitbucket、Bazaar、Mercurial或者SVN。
在Go 1.13版本,引入了一个新的环境变量GOPROXY,用于设置Go模块代理(Go module proxy)。模块代理可以使Go命令直接从代理服务器下载模块。GOPROXY默认值为
https://proxy.golang.org,direct ,代理服务器可以指定多个,中间用逗号隔开,例如
GOPROXY=https://proxy.golang.org,https://goproxy.cn,direct 。当下载模块时,会优先从指定的代理服务器上下载。如果下载失败,比如代理服务器不可访问,或者HTTP返回码为
404 或
410 ,Go命令会尝试从下一个代理服务器下载。
direct是一个特殊指示符,用来指示Go回源到模块的源地址(比如GitHub等)去抓取 ,当值列表中上一个Go module proxy返回404或410,Go会自动尝试列表中的下一个,遇见direct时回源,遇见EOF时终止,并抛出类似
invalid version: unknown revision… 的错误。 如果
GOPROXY=off ,则Go命令不会尝试从代理服务器下载模块。
引入Go module proxy会带来很多好处,比如:
- 国内开发者无法访问像golang.org、gopkg.in、go.uber.org这类域名,可以设置GOPROXY为国内可以访问的代理服务器,解决依赖包下载失败的问题。
- Go模块代理会永久缓存和存储所有的依赖,并且这些依赖一经缓存,不可更改,这也意味着我们不需要再维护一个vendor目录,也可以避免因为维护vendor目录所带来的存储空间占用。
- 因为依赖永久存在于代理服务器,这样即使模块从互联网上被删除,也仍然可以通过代理服务器获取到。
- 一旦将Go模块存储在Go代理服务器中,就无法覆盖或删除它,这可以保护开发者免受可能注入相同版本恶意代码所带来的攻击。
- 我们不再需要VCS工具来下载依赖,因为所有的依赖都是通过HTTP的方式从代理服务器下载。
- 因为Go代理通过HTTP独立提供了源代码(.zip存档)和go.mod,所以下载和构建Go模块的速度更快。因为可以独立获取go.mod(而之前必须获取整个仓库),所以解决依赖也更快。
- 当然,开发者也可以设置自己的Go模块代理,这样开发者可以对依赖包有更多的控制,并可以预防VCS停机所带来的下载失败。
在实际开发中,我们的很多模块可能需要从私有仓库拉取,通过代理服务器访问会报错,这时候我们需要将这些模块添加到环境变量GONOPROXY中,这些私有模块的哈希值也不会在checksum database中存在,需要将这些模块添加到GONOSUMDB中。一般来说,我建议直接设置GOPRIVATE环境变量,它的值将作为GONOPROXY和GONOSUMDB的默认值。
GONOPROXY、GONOSUMDB和GOPRIVATE都支持通配符,多个域名用逗号隔开,例如
/*.example.com,github.com 。
对于国内的Go开发者来说,目前有3个常用的GOPROXY可供选择,分别是官方、七牛和阿里云。
官方的GOPROXY,国内用户可能访问不到,所以我更推荐使用七牛的
goproxy.cn ,
goproxy.cn 是七牛云推出的非营利性项目,它的目标是为中国和世界上其他地方的Go开发者提供一个免费、可靠、持续在线,且经过 CDN 加速的模块代理。
指定版本号下载
通常,我们通过
go get 来下载模块,下载命令格式为
go get <package[@version]> ,如下表所示:
你可以使用
go get -u 更新package到latest版本,也可以使用
go get -u=patch 只更新小版本,例如从
v1.2.4 到
v1.2.5 。
按最小版本下载
一个模块往往会依赖许多其他模块,并且不同的模块也可能会依赖同一个模块的不同版本,如下图所示:
在上述依赖中,模块A依赖了模块B和模块C,模块B依赖了模块D,模块C依赖了模块D和模块F,模块D又依赖了模块E。并且,同模块的不同版本还依赖了对应模块的不同版本。
那么Go Modules是如何选择版本的呢?Go Modules 会把每个模块的依赖版本清单都整理出来,最终得到一个构建清单,如下图所示:
上图中,rough list和final list的区别在于重复引用的模块 D(
v1.3 、
v1.4 ),最终清单选用了D的
v1.4 版本。
这样做的主要原因有两个。第一个是语义化版本的控制。因为模块D的
v1.3 和
v1.4 版本变更都属于次版本号的变更,而在语义化版本的约束下,
v1.4 必须要向下兼容
v1.3 ,因此我们要选择高版本的
v1.4 。
第二个是模块导入路径的规范。主版本号不同,模块的导入路径就不一样。所以,如果出现不兼容的情况,主版本号会改变,例如从v1变为v2,模块的导入路径也就改变了,因此不会影响v1版本。
go.mod和go.sum介绍
在Go Modules中,go.mod和go.sum是两个非常重要的文件,下面我就来详细介绍这两个文件。
go.mod文件介绍
go.mod文件是Go Modules的核心文件。下面是一个go.mod文件示例: module github.com/marmotedu/iam go 1.14 require ( github.com/AlekSi/pointer v1.1.0 github.com/appleboy/gin-jwt/v2 v2.6.3 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 github.com/gin-gonic/gin v1.6.3 github.com/golangci/golangci-lint v1.30.0 // indirect github.com/google/uuid v1.0.0 github.com/blang/semver v3.5.0+incompatible golang.org/x/text v0.3.2 ) replace ( github.com/gin-gonic/gin => /home/colin/gin golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2 ) exclude ( github.com/google/uuid v1.1.0 )
接下来,我会从go.mod语句、go.mod版本号、go.mod文件修改方法三个方面来介绍go.mod。
- go.mod语句
go.mod文件中包含了4个语句,分别是module、require、replace 和 exclude。下面我来介绍下它们的功能。
- module:用来定义当前项目的模块路径。
- go:用来设置预期的Go版本,目前只是起标识作用。
- require:用来设置一个特定的模块版本,格式为
更多学习
更多实时资讯,前沿技术,生活趣事。尽在【老马啸西风】
交流社群:[交流群信息](https://mp.weixin.qq.com/s/rkSvXxiiLGjl3S-ZOZCr0Q)