Go Module
Go Module 是在 golang 1.11
中加入的一个依赖管理工具,在golang 1.11
中需要通过设置环境变量export GO111MODULE=on
来开启这个功能,到了golang 1.13
开始之后就成为了golang
默认的依赖管理工具了
Go 依赖管理的演化
GOPATH
依赖管理
在早期Golang
是使用$GOPATH
作为依赖管理的机制,在$GOPATH
目录下面有三个子目录:
- bin:一般用于存放可执行文件
- pkg:存放静态连接库文件
- src:存放第三方依赖的源码和自己开发的代码
一个$GOPATH
简单的目录树如下所示:
.
├── bin
│ ├── glide
│ ├── gocode
│ ├── ...
├── pkg
│ └── linux_amd64
└── src
├── github.com
└── gopkg.in
├── workspace
│ ├── project1
│ │ ├── .git
│ │ ├── main.go
│ ├── project2
$GOAPTH
也同样具有项目管理的机制,一个$GOPATH
目录就相当于一个工作区,当然也可以设置其他$GOPATH
添加一个新的工作区,但这样就需要在不同的$GOPATH
之间进行切换,会很麻烦了;
在使用GOPATH
进行依赖管理,规定了依赖的下载路径就是在$GOPATH/src
下面,当程序编译的时候就会到$GOPATH/src
下面找到相关的依赖进行编译和链接
GOPATH
依赖管理的问题
假如使用基于GOPATH
的依赖管理机制,你创建了一个Go
程序,在写该程序的时候引入了依赖D
,你使用命令go get
获取到了依赖D
的最新版本1.0.1
(因为基于GOPATH
的依赖机制没有版本感知的,所以就会拉取最新版本),成功的将应用运行起来了
过了一段时间,要添加新的功能,此时你又需要依赖C
,所以你再次使用go get
下载下来了C
的最新版本1.8
,完成之后,你开心的点击运行,结果程序突然奔溃了,花了一段时间终于确定了问题
问题就是在C
中也依赖了D
,但是在你执行go get C
命令时,在本地找到了D
,所以就不再去拉取D
的其他版本了;而C
代码中依赖的时D
的版本v1.0.4
,在这个版本解决了前面版本的一些bug
,所以C
可以正常使用,而现在c
用的是D
的v1.0.1
版本,所以就出错了
为了解决这个问题,你打算使用命令go get -u
将依赖更新到最新版本,之后你又开心的点击了运行,发现还是出错了,一段时间后,你发现了在D
的最新版本1.1.6
又引入了一个Bug
导致C
不能正常工作;逐渐的你失去了耐心,这一切都是因为基于GOPATH
依赖管理没有版本的概念
因此,go
官方在go 1.5
引入了vendor
依赖管理机制,同时在Go
社区也出现了其他的依赖管理机制,如:dep
Go Vendor
依赖管理
vendor
机制其实就是在项目的根目录下面添加了一个vendor
目录,然后将该项目的依赖缓存在了vendor
目录下面,项目的目录结构大致如下所示:
.
├── main.go
└── vendor/
├── github.com/
│ └── astaxie/
│ └── beego/
└── golang.org/
└── x/
└── net/
当编译程序的时候首先会在当前项目的vendor
目录下面去查找依赖,如果找不到才会去到$GOPATH/src
下面去找;verdor
是这个项目独有的依赖,而$GOPATH/src
是当前$GOPATH
下面多个项目所共享的依赖
goverdor
基本功能
在verdor
机制中,可以使用vendor/vendor.json
进行包和版本的管理,同样vendor
也提供了一些命令来管理项目中的依赖
govendor init
:初始化vendor
,会在项目的根目录下面创建vendor
和vendor/vendor.json
govendor add +external
:将项目中引用到的依赖从$GOPATH
中拷贝到vendor
目录下面govendor add gopkg.in/yaml.v2
:从$GOPATH
中拷贝指定的包到vendor
目录下面govendor list
:列出项目中引用到的依赖govendor list -v fmt
:列出一个包被引用的列表govendor fetch golang.org/x/net/context
:从远程下载依赖,依赖不会添加到到$GOPATH
govendor fetch golang.org/x/net/context@v1
:从远程下载指定版本的依赖govendor fetch golang.org/x/net/context@=v1
:从远程下载指定tag
或branch
的依赖govendor test +local
:测试项目中的test
用例govendor fetch +out
:拉取所有到vendor
目录当中govendor update +vendor
:从$GOPATH
更新依赖govendor sync
:同步vendor.json
中的依赖信息
vendor
机制的缺点
虽然vendor
目录解决了一些$GOPATH
中依赖的版本管理的问题,但是还是有一些缺点使得体验不是很好;
首先,vendor
目录的使用前提就是项目必须在$GOPATH/src
目录下面,这还是和GOPATH
一样的
其次,之前的$GOPATH
中的依赖是多个项目共享,现在使用vendor
机制后,每个项目都要维护自己的一套依赖,这就会占用大量的存储;所以,当你推送项目到远程仓库时,最好不要推送这些依赖,只要保证推送的项目有vendor
和vendor.json
就好了,别人拉取你的代码之后,就可以通过vendor.json
自己下载依赖了
最后,虽然vendor
提供了一些命令管理依赖,但是大部分时间还是需要手动的去管理项目依赖的依赖,包括依赖的版本记录,获取和存放等
因此,Go
官方为了能够解决这个问题,在go 1.11
版本引入了Go Module
来管理依赖
Go Module
Go Module
是从Go 1.11
版本开始新增的一种依赖管理机制,Go Module
是基于package
的,package
就是由一个文件或者多个文件实现的单一功能,在一个项目当中,会包含一个或多个package
;
一个Go Module
就是一个项目中这些package
的集合,它们可以组成一个独立的版本单元,它们可以一起打版本,发布和分发;在根目录下面有go.mod
文件,定义了module path
和依赖的版本
通常一个项目都是只有一个module
,项目主文件夹下包含go.mod,子文件夹定义package,或者主文件夹也是一个package。但是一个项目也可以包含多个module,只不过这种方式不常用而已
Go Module
的go.mod
文件
在Go Module
中,存在一个文件go.mod
,在这个文件中定义了项目所需要的所有依赖和对应的版本,通常这个文件都会在项目的主目录下面,一般格式如下:
module project
go 1.17
require (
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/aws/aws-sdk-go v1.32.5
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0
github.com/bugsnag/bugsnag-go v1.5.2 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.etcd.io/etcd/client/v2 v2.305.0-rc.1
go.etcd.io/etcd/client/v3 v3.5.0-rc.1
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b // indirect
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 // indirect
)
replace (
github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.3+incompatible
github.com/goharbor/harbor => ../
google.golang.org/api => google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff
)
exclude (
go.etcd.io/etcd/client/v2 v2.305.0-rc.0
go.etcd.io/etcd/client/v3 v3.5.0-rc.0
)
retract (
v1.0.0 // 该版本已被弃用,但是 tag 仍然在,如果你想继续使用,则可以在此指定
)
go.mod
文件的第一行是module path
,格式一般为:仓库名称+module name,如果是本地开发的,一般只有 module name
,如果需要发布到远程仓库,比如:github
,则需要添加为:github.com/xxx/moduleName
;
如果你发布的版本已经大于 2.0.0
,那按照 Go 规范
,你应该在后面加上版本的major
,改为:github.com/xxx/moduleName/v2
,在别人引用时也需要加上后面的v2
go.mod
文件中,接下来就是指定当前module
使用到的最低的go
版本,当然这个也可以不写
go 1.17
Require
在go.mod
的require
部分列出了项目所依赖的库以及对应的版本,在require
部分除了github.com/aws/aws-sdk-go v1.32.5
这种正常的依赖,还有一些加了后缀和注释的奇奇怪怪的依赖的定义
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0
上面的这个依赖库的版本是由go module
为其生成的一个伪版本号,该版本号符合语义化版本,实际上这个库没有发布这个版本
因为go module
需要确定每个依赖库的版本,所以如果这个库没有版本,那么go module
就会为其生成一个版本号,这个版本号中20140604031826
为该库这次提交的时间,e87155e8f0c0
就是这次提交的 commit id
,这样就能确定这个库的一个版本
当然,如果该项目依赖了一个本地库,后面的这两个就都是零值,比如:v0.0.0-00010101000000-000000000000
,00010101000000
是时间的零值
前面的v0.0.0
可能有多种生成方式,主要看你这个commit的base version:
- vX.0.0-yyyymmddhhmmss-abcdefabcdef: 如果没有base version,那么就是vX.0.0的形式
- vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef: 如果base version是一个预发布的版本,比如vX.Y.Z-pre,那么它就用vX.Y.Z-pre.0的形式
- vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef: 如果base version是一个正式发布的版本,那么它就patch号加1,如vX.Y.(Z+1)-0
indirect 注释
github.com/bugsnag/bugsnag-go v1.5.2 // indirect
依赖后面的indirect
注释表示当前的依赖不是直接依赖,而是一个间接依赖,也就是说,当前项目依赖了A
,A
又依赖了B
,那么这个依赖就是间接依赖,但也不是所有的间接依赖都会出现在当前项目的go.mod
中,有以下两种情况就会出现:
- 当前项目依赖的库没有启用
Go module
(最近发现go 1.17
开始,如果项目的依赖没有go.mod
文件就会报错) - 当前项目依赖的库启用了
go module
,但是有部分依赖没有在go.mod
文件当中
incompatible
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
有些库的版本后面加了incompatible
,但是当你去看这个依赖的时候发现其tag
中没有incomptaible
;这表明当前这个库的major
版本已经大于2
了,但是在它的module path
中并没有添加v2 v3
这样的后缀;go module
之所在版本后面添加incompatibal
就是因为其不符合规范,当然你也可以正常使用
exclude
exclude (
go.etcd.io/etcd/client/v2 v2.305.0-rc.0
go.etcd.io/etcd/client/v3 v3.5.0-rc.0
)
如果你想在当前项目中跳过某个依赖指定的版本,那么就可以将这个版本放入到exclude
模块当中,这样当你使用go get
时就会自动跳过这个版本了
retract
retract (
v1.0.0 // 该版本已被弃用,但是 tag 仍然在,如果你想继续使用,则可以在此指定
)
如果第三方发布了一个库的版本,后来发现这个版本不稳定想撤回,并且推送一个新的版本,但这个撤回的版本的 tag
还在
如果你想强制使用,就可以将这个版本放入到retract
下面,否则这些撤回的版本就会被自动跳过了
Go Module
常用命令
go mod init moduleName
:初始化一个go module
,会在当前目录下生成go.mod
文件go mod tidy
:清除不需要的依赖,更新依赖到go.mod
文件中指定的版本go mod download
:下载依赖到本地缓存go mod vendor
:将依赖拷贝到vendor
目录中go mod edit
:编辑go.mod
中的依赖go mod graph
:打印该module
的依赖关系图go mod verify
:校验一个模块的依赖是否被篡改过go mod why -m <pkg>
:查看该依赖为什么存在go list -u -m all
:查看所有已升级的依赖go mod edit -fmt/-require=/-droprequire=
:格式化/添加依赖/移除依赖项
注:go 1.15
版本的时候还可以在vendor
中放入本地依赖,在go.mod
中引用,比如:
replace test-sdk => ./vendor/test-sdk
而到了go 1.17
开始就不在允许将本地依赖放到vendor
目录下面了,只能通过本地磁盘来引入,比如:
replace test-sdk => /home/go/GOPATH/src/test-sdk
这样导致了代码提交到远程仓库之后,别人编译时还需要再将本地依赖重新引入一遍,否则就会出现编译失败的问题;不知道有没有好的解决办法
Go Module
的依赖管理机制
Go
语言在设计Go Module
的时候,引入了语义化版本机制和最小版本选择算法,语义化版本控制就是依赖发布的版本格式的定义,具体可在语义化版本规范2.0.0文档查看,或者博客最后简单的介绍
最小版本选择是当该module
或者module
的依赖引入同一个依赖的不同版本时go module
做出的版本选择的一个算法,这个算法就是基于依赖遵循了语义化版本的规范
假如,现在有一个module A
依赖了module B
和module C
,module B
的不同版本依赖module D
的不同版本,module C
依赖了module D
和module F
,而module D
又依赖于module E
,那这个时候go module
如何选择使用module D
的哪一个版本呢?
根据最小版本选择算法,以及语义化版本的含义,go module
会选择module D 1.4
作为该module
的依赖;因为在语义化版本控制中module D
是次版本的变化,go module 1.4
会向下兼容,所以就会选择该版本作为这些module
的依赖版本
从上面的最小版本选择机制中可以看出,go module
选择的依赖是最大非最新的版本
,当然这种的前提是该依赖符合语义化版本控制的规则,否则也有可能出现依赖不兼容的问题
Go Module 缓存
项目中开启 go module
之后,执行go get
下载的依赖就会被缓存在$GOPATH/pkg/mod
中,在Go1.15
之后就可以使用环境变量GOMODCACHE
来设置缓存路径了
为了保证下载的依赖的安全性,Go Module
就会在你的依赖变动的时候去到一个sumdb
服务器中进行校验,这个服务器的地址就存在$GOPATH/pkg/sumdb
目录下面
Go1.13 中当设置了 GOPROXY="proxy.golang.org" 时 GOSUMDB 默认指向 "sum.golang.org",其他情况默认都是关闭的状态。如果设置了 GOSUMDB 为 “off” 或者使用 go get 的时候启用了-insecure
参数,Go 不会去对下载的依赖包做安全校验,存在一定的安全隐患,所以给大家推荐接下来的环境变量。
如果你的代码仓库或者模块是私有的,那么它的校验值不会出现在互联网的公有数据库里面,但是我们本地编译的时候默认所有的依赖下载都会去尝试做校验,这样不仅会校验失败,更会泄漏一些私有仓库的路径等信息,我们可以使用GONOSUMDB
这个环境变量来设置不做校验的代码仓库, 它可以设置多个匹配路径,用逗号相隔.;如:
GONOSUMDB=*.corp.example.com,rsc.io/private
这样的话,像 "git.corp.example.com/xyzzy", "rsc.io/private", 和 "rsc.io/private/quu…"这些公司和自己的私有仓库就都不会做校验了。
Go Module 的不断变化
目前从Go
官方引入Go Module
开始之后,在后面每一个Go
版本中都会对Go Module
进行一些改变,这种不断的变化无形中增加了学习的成本
go get 和 go install 之间的区别
从 Go 1.7 开始,就不在使用 go get
去安装二进制文件了,而是使用Go install
来安装文件了;当然,也有一个参数就是-d
,就是可以使用go get -d
来达到和之前的go get
一样的目的
什么是语义化版本
语义化版本就是规定了一个应用发布的版本格式,每个字段的意义以及版本号比较的规则等等,下面简单的介绍一下
版本格式
下面是一个语义化版本的格式
//major.minor.patch-beta+metdata
v1.2.3
主版本号的变动:当你做了对之前完全不兼容更改时,比如从v1.0.1
到v2.0.1
次版本号变动:此版本号的更改表示实现了向下兼容的新功能的添加,比如从v1.1.4
到v1.2.0
修改版本号变动:此版本号的更改表示实现了向下兼容的问题的修正,比如从v1.1.5
到v1.1.6
参考连接: