原始依赖管理方式
1.Go语言可以利用本身的能力做基础依赖管理,几个重要的组件包括GOPATH工作目录,Go命令工具(get install build)等,通过go get
下载依赖包的最新版本到GOPATH
指定目录。
Go语言有个重要的环境变量 GOPATH
,保存工作目录路径(working space),此目录下存放正在开发的代码,编译后的依赖包,依赖包的源码以及可执行文件。GOPATH
默认路径是$HOME/go
,可以修改指向路径,但是,设置时不能修改成Go的安装目录(GOROOT
)。 代码里面import
非语言自带的包时,从GOPATH
目录查找。
bin/ # 可执行文件 hello # command executable outyet # command executable pkg/ # go mod依赖,编译过程文件等 src/ # 存放正在开发的源码 golang.org/x/example/ # 此目录也称为 `import path` .git/ # Git repository metadata hello/ hello.go # command source outyet/ main.go # command source main_test.go # test source stringutil/ reverse.go # package source reverse_test.go # test source golang.org/x/image/ .git/ # Git repository metadata bmp/ reader.go # package source writer.go # package source
$GOPATH/pkg
Go mod启用前,预编译好的依赖包,加快编译速度,如果遇到编译问题,可以删掉此目录,Go会重建。启用Go mod后,依赖包存放在此目录,参考下面目录结构。
├── pkg ├── darwin_amd64 # 处理器架构 │ └── github.com │ ├── cpuguy83 │ │ └── go-md2man.a # 编译的中间文件 │ └── urfave │ └── cli.a │ ├── mod # 依赖模块 │ │ ├── cache │ │ │ ├── download │ │ │ ├── lock │ │ │ └── vcs │ │ ├── cloud.google.com │ │ │ ├── [email protected] │ │ │ └── [email protected] │ │ ├── git.apache.org │ │ │ └── [email protected]
go get
go install
go build
存在的问题
虽然,Go的基础组件能够一定程度地支持依赖管理,不过也存在明显的缺陷:
- 无法有效管理依赖包的不同版本,go get只能获取到最新版本的包,由于无法指定依赖版本,依赖包的源码仓库有变动,必然会造成意外影响(更新依赖发现不能编译了,多人开发依赖版本不一致导致的bug等)
- 无法达成同一个项目,依赖包的不同版本,比如,你要做ElasticSearch迁移,从ElasticSearch6迁移到ElasticSearch7,这两个版本的client sdk有不兼容。想象下,如果不支持同时依赖多个版本,对于开发者来说,要么在依赖包的代码仓库层做文章(比如,不同仓库,不同目录等),要么下载依赖包后,在包的存储目录上处理,效率太低
godep方案
为了解决依赖管理的问题,从1.5版本开始,Go引入了vendoring
机制,简单来说,就是把项目中依赖到的模块,直接拷贝到项目目录中,跟随项目源码一起做版本管理,不过有个问题是,go get
并不会把依赖直接下载到项目的vendor
目录里去,需要人肉处理,或者使用工具godep。
使用vendoring
的话,项目目录参考,每个包最好都维持自身的vendor
目录,确保依赖可控。
项目中依赖的解析规则:
- 优先检测当前目录是否有
vendor
目录,有的话从此目录查找依赖 - 如果同目录没有,则往上层目录查找,
- 要么找到依赖
- 要么找到一直到
$GOPATH/src
还没有找到 - 如果第二步没有找到,那么,从
GOROOT
目录找 - 如果还没找到,那么,从
$GOPATH/src
继续查找 - 失败 or 找到依赖
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go # import user/package-one,依赖的是同目录vendor下的包 vendor/ github.com/user/package-one/ one.go client/ client.go # import user/package-one,依赖的是同目录vendor下的包 vendor/ github.com/user/package-one/ server/ server.go # import user/package-one,依赖的是同目录vendor下的包 vendor/ github.com/user/package-one/ one.go
项目开发过程中,可以使用godep save
命令,将依赖从GOPATH/src/path/to/package
拷贝到项目的vendor
目录。同时,生成一份依赖列表文件存放在Godeps/Godeps.json
中,如下:
{ "ImportPath": "github.com/tools/godep", "GoVersion": "go1.7", "GodepVersion": "v74", "Deps": [ { "ImportPath": "github.com/kr/fs", "Rev": "2788f0dbd16903de03cb8186e5c7d97b69ad387b" }, { "ImportPath": "github.com/kr/pretty", "Comment": "go.weekly.2011-12-22-24-gf31442d", "Rev": "f31442d60e51465c69811e2107ae978868dbea5c" }, { "ImportPath": "github.com/kr/text", "Rev": "6807e777504f54ad073ecef66747de158294b639" }, { "ImportPath": "github.com/pmezard/go-difflib/difflib", "Rev": "f78a839676152fd9f4863704f5d516195c18fc14" }, { "ImportPath": "golang.org/x/tools/go/vcs", "Rev": "1f1b3322f67af76803c942fd237291538ec68262" } ] }
godep save
后,依赖代码可以一起纳入版本管理,递交到git仓库。godep restore
命令是反向操作,将Godeps/Godeps.json
中指定的依赖包/版本,恢复到$GOPATH
。 如果依赖的包有版本更新,可以用go get -u
更新包,再通过godep save
将依赖拷贝到项目中去。
Go1.9推出了官方试验方案dep,功能类似,不再细述。
官方解决方案 go mod
Go mod目前是Go的官方解决方案,Go 1.11(2018年8月) 引入GO111MODULE
环境变量,默认值为auto
。如果设置该变量 GO111MODULE=off
,那么Go命令将始终使用GOPATH mode
开发模式。如果设置该变量GO111MODULE=on
,Go命令将始终使用Go Modules开发模式。Go 1.13开始成本默认的包管理工具。
模块是指一组相关的包,这些包一起发布,版本号一致,可以直接从版本管理仓库(比如git)下载,也可以通过proxy代理服务器下载到本地。类似maven
的包管理,每个包/模块都有自己的坐标
,唯一标识。Go mod中使用module path来唯一标识模块,其定义在go.mod
文件中,此文件也包括了模块的依赖信息。模块中的包是一些在相同目录中的go源码文件,包也有自己的标识/路径,由module path和包所在的目录组成,比如,模块golang.org/x/net
,其中html
包的路径就是golang.org/x/net/html
。
go module golang.org/x/net
的定义:
➜ [email protected] cat go.mod module golang.org/x/net go 1.11 require ( golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a golang.org/x/text v0.3.0 ) x/net目录结构: ├── [email protected] │ ├── AUTHORS │ ├── CONTRIBUTING.md │ ├── CONTRIBUTORS │ ├── LICENSE │ ├── PATENTS │ ├── README.md │ ├── bpf │ ├── codereview.cfg │ ├── context │ ├── dict │ ├── dns │ ├── go.mod │ ├── go.sum │ ├── html
module path
module path描述了模块的作用,也说明了模块的位置(从哪里可以找到这个模块),module path定义在go.mod
文件中,下面的示例,module path是github.com/hashicorp/raft/v2
module github.com/hashicorp/raft/v2 go 1.12 require ( github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 github.com/hashicorp/go-hclog v0.9.1 github.com/hashicorp/go-msgpack v0.5.5 github.com/stretchr/testify v1.3.0 )
一般来说,module path包含三部分:
- 代码仓库根目录,大部分模块的目录就是仓库的根目录(
/
),比如,golang.org/x/net
模块,其代码仓库根目录就是模块目录,模块就定义在仓库的根目录下 - 模块目录,模块没有定义在仓库的根目录下,有独立的目录,此目录名作为module path的一部分,比如模块
golang.org/x/tools/gopls
,模块内容在仓库golang.org/x/tools
的gopls
目录下 - 主版本信息,如果模块的主版本>=2,那么,模块名需要加上主版本信息,比如
/v2
。这里特别要注意的一点,/v2
可以是仓库下的独立目录,也可以是仓库的根目录,比如,module pathgolang.org/x/repo/sub/v2
,可以是定义在golang.org/x/repo
的/sub
目录,也可以是定义在/sub/v2
目录下。如果是定义在/sub/v2
目录下,那么,此模块的代码仓库用同一份代码,维护了多个版本,/sub/v2
维护了/v2
版本,/sub
维护了另外一个版本
module version
模块版本名以字母v
开头,命名规则:
v{major}.{minor}.{patch}-prerelease v{major}.{minor}.{patch}+meta major = 0 非稳定版本
prerelease预发布版本,版本排序时,排在v{major}.{minor}.{patch}之前,prerelease版本一般认为是不稳定的版本,使用时需要注意是否有兼容性、逻辑问题。
meta 在比较版本时,meta不参与比较。由于历史原因,很多go模块在开始开发的时候,并没有引入module机制,这些go模块后续使用包机制时,直接从版本2开始引入,这些模块的v2
版本一般会加上+incompatible
标记。
Pseudo-versions
一般用在预发布版本上,直接利用代码仓库的版本信息转化为模块版本,比如v0.0.0-20191109021931-daa7c04131f5
。 在实际使用时,如果依赖特定commit版本的包,可以直接指定:
go get -d example.com/mod@master go list -m -json example.com/mod@abcd1234
Major version suffixes
如果新版本和老版本的import path一致,那必须确保这两个版本是兼容的。为了做到这点,从v2
版本开始,需要重新定义import path才可以。
有个特例,如果module path是gopkg.in/
开头,那么,从版本0开始,import path就需要加上主版本信息,但不是/v2
这样的形式,而是.v2
,比如,gopkg.in/yaml.v2
。
常用命令
初始化 go mod init
,执行后,在当前目录内创建go.mod
文件:
module github.com/you/hello go 1.12
增加依赖 go get github.com/go-chi/[email protected]
module github.com/you/hello go 1.12 require github.com/go-chi/chi v0.9.1
替换依赖模块
有的时候,我们可能会需要依赖修改过的模块,比如,我们在开发web框架,依赖了gin框架,但是,开发过程中,需要修改一些gin框架的代码,我们就可以用我们自己维护的gin版本来做。使用replace
命令,替换相关依赖,go mod edit -replace github.com/my/library ../path
# go.mod module github.com/myorg/app require ( github.com/thirdpart/library v1.1.0 ) replace github.com/myorg/library => ../path
修改过后,后续每次编译都会使用replace
后的代码来编译,如上../path
下的代码,后续,开发结束后,可以把对应replace
依赖去掉,go mod edit -dropreplace github.com/my/library
。
清理依赖 go mod tidy