Go 依赖管理详解

1,475 阅读3分钟

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用的是Dv1.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,会在项目的根目录下面创建vendorvendor/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:从远程下载指定tagbranch的依赖
  • 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机制后,每个项目都要维护自己的一套依赖,这就会占用大量的存储;所以,当你推送项目到远程仓库时,最好不要推送这些依赖,只要保证推送的项目有vendorvendor.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 Modulego.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.modrequire部分列出了项目所依赖的库以及对应的版本,在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-00000000000000010101000000是时间的零值

前面的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注释表示当前的依赖不是直接依赖,而是一个间接依赖,也就是说,当前项目依赖了AA又依赖了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 Bmodule Cmodule B的不同版本依赖module D的不同版本,module C依赖了module Dmodule F,而module D又依赖于module E,那这个时候go module如何选择使用module D的哪一个版本呢?

dp.jpg

根据最小版本选择算法,以及语义化版本的含义,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.1v2.0.1

次版本号变动:此版本号的更改表示实现了向下兼容的新功能的添加,比如从v1.1.4v1.2.0

修改版本号变动:此版本号的更改表示实现了向下兼容的问题的修正,比如从v1.1.5v1.1.6

参考连接:

colobu.com/2021/06/28/…

blog.51cto.com/niuben/5269…