Go语言依赖管理

Go语言依赖管理

GOPATH

📌GOROOT

我们安装Go,其实是安装它的编译器标准库,二者位于同一个安装包下。

GOROOT目录则是Go语言的顶级目录,包括了开发Go应用程序所需的所有组件,包括编译器,标准库,文档。

同时,系统的Path变量还会添加一个GOROOT\bin目录,这里存放Go语言开发包中提供的二进制可执行程序。

📌各种package

Go语言程序其实是由多个package构成的,类似于Java是由多个包构成的,Go的package来源可以分为一下几种:

  • 标准库—GOROOT下
  • 第三方库
  • 私有库

第三方库和私有库则位于GOPATH环境变量所定义的目录中

📌GOPATH

安装Go语言,会默认设置一个GOPATH = %USERPROFILE%\go

这就暗示了,GOPATH环境指向一个用户域,每个用户都可以拥有自己的独立空间来管理自己所需要使用的第三方库和私有库。但是这也要求了用户的Go项目需要位于GOPATH下的src\目录中。

当我们的程序要引用其他包时,Go编译器会依次从GOROOT\src 和 GOPATH\pkg中去找,先找到就停止。

GOPATH缺点:

如果有人开发2个Go程序,但是使用了一个包的2个版本,GOPATH下是不能同时保存的,因此需要针对2个项目创建2个GOPATH,非常不方便。

vendor

Go1.6开始提供的新机制,虽然无法让多个项目共享同一个GOPATH,但是它提供了1个机制让项目的依赖隔离互不干扰。

由于我是用的Go 版本更高,已经不使用vendor了,所以这部分内容就不详细了解。

Go Module

Go1.11开始,官方提供了Go Module来管理依赖。这是一种全新的依赖管理方案,解决了GOPATH和vendor的问题。

Go Module基础

Module是什么?

给个🌰

import "github.com/blang/semver"
import "github.com/google/uuid"

这个import引用最终结果其实就是引用了一个module,github.com下的blang这个仓库,包含了一个semver的模块,这个模块下可能还有多个package,每个package都有1个或多个源文件。

对于module和package的概念关系大致就是上面这样。

并且,module是有版本号的,且有语义化规范。v(major).(minor).(patch),例如v0.1.0, v1.5.0-rc.1

major 大版本 — 不兼容的改动

minor 小版本 — 新特性新功能

patch 补丁版本🍮 — bug修复

快速实践

📌初始化module

自己的项目,也要成为一个module,这样Go Module才能统一管理,初始化方式如下:

在项目下直接创建一个go.mod文件,然后按照如下格式定义自己的module名

注意:我这个名字是xxx/yyy/zzz,那就是说,我这个module名其实是zzz,仓库名是yyy,放到xxx这个仓库管理平台上,类似于上面的github。

同时这个很全的module名也决定了我的这个项目要这么放:

其中,iqiyi-go-project是我这个项目的GOPATH

定义和安排好了module,然后就采用命令(在qke-generic-be这个目录下)去构建这个module

go mod init

这样,我们一个go项目就初始化完成了

📌管理依赖

如果我们的项目中要引入第三方包,比如

import "github.com/blang/semver"
import "github.com/google/uuid"

那么我们运行程序之前要先将这个包下载下来,也就是下载到本项目的GOPATH/pkg 中

命令:

go get

这个命令会自动分析和下载依赖包,但是下载的都是最新版本的。同时这个命令也会修改go.mod文件,将依赖信息添加到文件中require

第一次引用第三方库还会生成go.sum文件,进一步标记第三方库的版号和Hash值,便于其他人下载和我同样的依赖(这一点非常重要,一定要相同,如果不同可能就会出问题)。

另外,如果是从仓库中下载的项目,那么这个时候,go.mod文件已经很完善,我们就直接下载依赖即可:

go mod tidy

这个命令会把所有依赖都下载到本地

📌运行GO程序

项目根目录下必须有一个main.go文件,这是程序的开始文件

构建命令:

go build

运行命令:

go run main.go

replace指令

go.mod提供了4个指令来进行module控制

  • module:规定自己的module名称,注意这个名字直接决定本项目的路径,要规范
  • require:声明依赖的信息,包括名称和版号
  • replace:替换require中声明的依赖,使用另外的依赖名和版号
  • exclude:禁用指定的依赖

目前我用过上面的前3个,一般情况下,其实只要前2种就够用了(此处可以参考Java的常用maven管理方式)。但是还是要学习的。

由于比较简单,直接给个🌰

上面画箭头的就是经典使用方式,通过替换版本号来替换依赖

replace作用生效的2个条件🌟

  1. replace只作用在main module,比如我这个应用是qke-generic-be,我运行这个应用下载依赖过程中,repalce可以生效。但是如果是别人要依赖我这个应用,他的应用下载依赖的时候肯定会下载我这个应用,此时我的应用对他来说就不是main module了,那么我的go.mod文件中的replace对他来说就不生效了。
  2. repalce中 ⇒ 前面的依赖必须出现在require出现,否则本条repalce无效

replace的使用场景🌟

  1. 替换无法下载的依赖包

很经典的问题,哈哈

e.g.

replace (
    golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
)
  1. 使用本地依赖

  1. 使用fork仓库

如果发现开源仓库的包有bug,但是他们的开发人员由不能及时修复,那么我们可以fork他们的仓库,然后自己修改,这个时候就可以用replace来替换依赖。需要注意的是,这不是长久之计,一旦发现他们修复好了bug,要及时修改回去。

e.g.

github.com/google/uuid v1.1.1 => github.com/yyfbf/uuid v1.1.2
  1. 禁止被依赖

有的情况是,特殊的module不希望被直接引用,比如Kubernetes,在他的go.mod中有大量的v0.0.0依赖,由于不存在这种版本,所以其他项目想直接依赖k8s.io/kubernetes是会因为找不到依赖而无法使用(复习一下这是为什么?replace生效的第一个条件)。这是因为Kubernetes不想作为一个整体被引用,如果有需要,指定需要引用的具体模块即可,因此对外隐藏了真实的版本号,由使用者通过replace来自行决定要依赖哪个模块并指定真正需要的版本号。

k8s.io/kubernetes的例子

module k8s.io/kubernetes

require (
    ...
    k8s.io/api v0.0.0 // k8s的子模块
    k8s.io/apiserver v0.0.0
    ....

)


省略了replace,可以思考一下省略的这部分应该填什么

给一个实际使用的例子

有了以上的知识,就可以自己去运行一个go的项目了。如果想进一步了解的,可以接着看。

exclude指令

exclude用于排除某个包的特定版本

生效条件🌟

和replace相同,只在main module中生效

使用场景🌟

实际项目中很少使用,除非知道某个版本的包有明显bug,才回去排除。如果引用了其他包,其他包会用到那个版本,则exclude也会生效,去跳过exclude的版本,下载更新的版本。如果没有更新的版本,则会报错无法编译。

indirect指令

这个命令不是自己写的,而是编译器通过分析包之间的依赖关系自动加上去的。 表示这个包不是直接依赖/引用,而是间接依赖/引用。如下🌰

k8s.io/heapster v1.2.0-beta.1 // indirect

为什么要使用这个命令?

  • 有的go程序版本比较老,没有用到Go Module
  • 有的go程序的go.mod不规范,缺失了部分字段

indirect生效的场景🌟

  1. main module的直接依赖没有使用Go Module

此时A的go.mod文件为

require(
    B vx.y.z
    B1 vx.y.z // indirect
    B2 vx.y.z // indirect
)
  1. main module的直接依赖使用了Go Module,但是go.mod不完整

此时A的go.mod文件为

require(
    B vx.y.z
    B2 vx.y.z // indirect
)

补充:查看包之间的依赖关系

  • 查看单个包的依赖关系
go mod why -m <pkg>

输出指定pkg的依赖链

  • 查看所有包的依赖关系
go mod why -m all

版本选择机制

  1. 版本约定
  • Go Module之前

如果新的package和旧的package有相同的路径,则新package必须兼容旧的,如果想要修改旧的方法,建议创建新的方法(开闭原则)

如果新的package不能兼容旧的,则需要修改import路径

  • Go Module之后

除了上面的,还加了一条:

如果major版本号大于1,则major版本号需要显示标注在import路径中,例如github.com/my/mod/v2

好处:新旧package会被视为2个module,甚至可以同时引用

  1. 版本选择机制

这里主要了解Go的自动版本选择算法

  • 最新版本选择

如果代码中出现import,引入新的package,则go build或go test命令都会去找符合语义化最新版本的package,并在go.mod中增加一条require依赖。

  • 最小版本选择

如果A依赖M的1.0.0版本,但是A又依赖了D,D依赖M的1.1.1版本,此时会自动选择最小可用版本,即M的1.1.1版本,不会选择最新版本

incompatible

之前提到了一个约定,如果你的项目中使用了go.mod,并且你的major大于了1,而路径并没有根据约定的将major显式写在路径中,则说明你的module不够规范,此时别的模块引用你的这种版本的包就会出问题。

使用go mod tidy命令还是会找到最新的版本,但是为了区分这是不符合规范的module,会在go.mod中增加incompatible标识,如下🌰

github.com/go-mail/mail v2.3.1+incompatible

虽然不影响使用,但是,从Go 1.14开始,go get命令则不会自动选择不规范的版本。

因此,推荐规范化!

伪版本

go.mod通常使用语义化版本来标记依赖,也就是v1.4.2这种形式。而这种形式通常是某个commit ID的Tag,实际上这个Tag对应一个commit ID,而这个commit ID才是真正的版本。

当我们不使用语义化版本,而使用了commit ID,则这种版本称为伪版本(pseudo-version)。通常格式如下:

vx.y.z-yyyymmddhhmmss-abcdefabcdef

其中:vx.y.z看上去是一个真实的语义化版本,但通常不存在该版本,所以形式有很多,例如vx.y.z-pre.0, vx.y.z-0, vx.y.z-dev.2.0等。且vx.y.z取决于使用commit ID的上一个版号。

yyyymmddhhmmss则是commit ID对应的时间。abcdefabcdef则是commit ID的前12位。

通常,伪版本不需要手写,go会自动计算并填写伪版本。如果想使用伪版本,可以参考如下命令:

go get github.com/xxx/xxx@commit ID(或者前12)

此时就会获取到依赖的伪版本。比如🌰

  github.com/instrumenta/kubeval v0.0.0-20201118090229-529b532b1ea1

依赖包存储

之前已经演示过了下载的第三方依赖包存储在哪里,演示的是Go Module模式,下面再统一介绍一下相关知识。

  • GOPATH模式

go 1.11之前,存储位置在GOPATH/src目录下

  • GOMODULE模式

go 1.11之后,使用了go.mod来管理依赖,则存储位置为GOPATH/pkg/mod,该目录还有一个cache目录,存储依赖包的缓存

GOMODULE模式的特点:

①依赖包的目录包括了版本号,每个版本占用一个目录

② 依赖包的特定版本目录中只包含依赖包文件,不包含.git目录

③ 包名中如果有大写字母,则存储时会将大写字母转换为 “!+小写字母”。🌰

github.com/Azure → github.com/!azure

go.sum

go.sum记录的是依赖包的Hash值,如果本地依赖包的Hash值与记录的Hash值不同,则程序会拒绝构建。

  • 文件格式🌟

go.sum内每行记录格式如下:

<module> <version>[/go.mod] <hash>

e.g.

github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=

正常情况下,每个依赖包版本都会包含2条记录。

第一条记录:该依赖包版本整体的Hash值

第二条记录:该依赖包版本的go.mod文件的Hash值(如果没有go.mod文件,则没有本条记录)

其中h1表示该Hash值是由SHA-256算法计算得出。

此外,go.sum中记录的依赖包版本的数量比go.mod中要多。这是因为go.sum要记录构建用的所有依赖包。

  • 生成过程

①使用go get命令下载依赖

go get github.com/google/uuid@v1.0.0

go get命令会将压缩包下载到本地的缓存目录GOPATH/pkg/mod/cache/download下,该依赖包是一个.zip的压缩包,如下

②计算Hash值

然后go get命令还会对.zip包做Hash运算,将结果存放在.ziphash的文件中

③更新go.sum和go.mod

如果在项目的根目录执行go get命令,还会对这2个文件进行更新。在更新go.sum之前,为确保下载的依赖包可靠,go命令还会咨询GOSUMDB环境变量所指示的服务器,得到一个依赖包版本的Hash值,并和自己计算的比较,不一致则不更新go.sum文件。

④运行别的项目

当我们运行别人的项目,如果本地已经下载过同样的依赖包版本,go命令则会直接直接进行校验操作,也就是计算本地缓存中依赖包版本的Hash值,和go.sum文件中的Hash值进行比较。如果教研失败,需要确认一下是本地缓存错误还是go.sum的记录错误,二者都有可能出错。

  • 校验和数据库

环境变量GOSUMDB标识了一个checksum database,实际上是一个web服务器,用于查询依赖包版本的Hash值。如果禁用了这个,则依赖包版本的Hash值在写入go.sum之前不会进行二次校验。

其他内容

GOSUMDB工作机制、实现原理,第三方代理,私有模块,Go Module的演进。这些内容就不讲了,有的内容太过于底层,私有模块这部分主要是公司自己搭建,搭建完成就可以当第三方依赖管理来使用,类似于Java中每个公司都有自己的仓库,来到公司的第一件事就是替换maven的配置文件,这样就能使用公司内部的依赖,但是在日常编程中其实和使用普通的依赖感知是一样的。

参考

《Go专家编程》任洪彩

参考视频如下:
【Go语言依赖管理,快速运行一个简单的Go项目】 https://www.bilibili.com/video/BV1P64y1H7UJ/?share_source=copy_web&vd_source=2fc4a66fb7fe408b706bb10750c6ac28

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值