55.Go中的依赖管理Module使用
1.什么是GoModule?(Go中Module和包的区别?)
首先我们要理解一下Go的Module是一个什么概念?
我先简单的说一下,Go中的Module是GoSDK1.11后提出的一个新的类似于包的机制,叫做模块,在1.13版本后成熟使用,GoSDK中Module功能是和相当于一个包的增强版,一个模块类型的项目在根目录下必须有一个go.mod文件,这个模块项目内部可以有很多个不同的包,每个包下有不同的代码,我们下载依赖的时候是把这个模块下载下来(模块以压缩包(比如zip)的形式存储在{GOPATH}/pkg/mod/cache/下,源码文件也会在{GOPATH}/pkg/mod/下)。
我们导入模块的时候只需要引入一次,使用模块中不同的包的时候可以通过import模块下不同的包名,来引入不同包的功能。
比如下面的结构
-----------com.cc.module
-----------------------package1
--------------test1.go
------------------------package2
-------------test2.go
然后我们只需要在go.mod中引入这一个模块,就能在import的时候任意引入package1或package2。
2.为什么要使用GoModule?
1).团队协作开发中对于包的版本号的管理
在没有Module之前,我们都是把自己写的Go程序打成包,然后别的程序引用的话引入这个包。
可是在开发中这些包的版本有个明显的不能管理的问题。
比如我怎么知道这个包是今天开发的最新版还是明天开发的,我在团队协同开发中怎么把别人写的最新版本的包更新到我的项目中。
2)便于开发中的依赖包管理
其次,我们在开发中下载了别人的项目,怎么快速的观察有哪些依赖包,如何快速的把所有依赖包从仓库中下载下来,都是一个问题,
这两个问题就可以通过观察项目根目录下的go.mod文件的依赖模块列表和执行go mod download命令快速从第三方模块仓库中下载依赖包来完成。
3).隔离管理不同类别的项目
有了Module后,我们可以把我们自己的项目和系统的项目隔离管理,我们的项目不用必须放在${GOPATH}/src下了
3.哪些项目能使用GoModule?
一个GoModule项目要想引入其它依赖模块,需要在根目录下的go.mod中添加对应的依赖模块地址。
注意:!!!重点来了!!!
GoModule只能引用同样是Module类型的项目,经常用于引用内部自己的项目。
像maven仓库一样引用开源模块的依赖也是一个特别常用的场景。
不过我们需要修改代理地址访问国内的第三方GoModule提供商。
4.GoModule的版本问题?
我们使用Go module之前,首先要保证我们的Go SDK是在1.13以及以上版本。(Go1.11以上就可以使用Module,但是需要设置一些开启等,1.13后默认开启)
因为在1.13版本上官方正式推荐使用,成为稳定版了。
Go也有代码仓库,比如可以使用github作为go项目的代码仓库,Go语言本身就提供了一个指令 go get 来从指定的仓库中
拉取依赖包和代码,不过go get这个指令在开启模块功能和关闭模块功能下用法不一样,下面有开启模块下的用法。
5.GoModule和Java中Maven的区别?
Go中的Module和Java中的Maven不同:
首先,Module是官方的SDK包自带的,它并非像maven一样还得安装maven插件之类的。
关于中央依赖仓库,Go和Java中的概念是类似的,都是国内的第三方提供的。
6.如何开启GoModule?(GO111MODULE)
具体我们如何使用Module呢?
我们首先要检查我们的GoSDK版本是1.11还是1.13之上。
如果是1.11的话我们需要设置一个操作系统的中的环境变量,用于开启Module功能,这个是否开启的环境变量名是GO111MODULE,
他有三种状态:
第一个是on 开启状态,在此状态开启下项目不会再去${GOPATH}下寻找依赖包。
第二个是off 不开启Module功能,在此状态下项目会去${GOPATH}下寻找依赖包。
第三个是auto自动检测状态,在此状态下会判断项目是否在{GOPATH}/src外,如果在外面,会判断项目根目录下是否有go.mod文件,如果均有则开启Module功能,如果缺任何一个则会从{GOPATH}下寻找依赖包。
GoSDK1.13版本后GO111MODULE的默认值是auto,所以1.13版本后不用修改该变量。
注意:在使用模块的时候,GOPATH 是无意义的,不过它还是会把下载的依赖储存在 ${GOPATH}/src/mod 中,也会把 go install 的结果放在 ${GOPATH}/bin 中。
windows
set GO111MODULE=on
linux
export GO111MODULE=on
7.GoModule的真实使用场景1:
接下来我们代入具体的使用场景:
今天,小明要接手一个新的Go项目,他通过GoLand中的git工具,从公司的git仓库中下载了一个Go的项目。(下载到他电脑的非${GOPATH}/src目录,比如下载到他电脑的任意一个自己的工作空间)
此时他要做的是:
1).先打开项目根目录下的go.mod文件看看里面依赖了什么工具包。(这个就是随便了解一下项目)
2).Go的中央模块仓库是Go的官网提供的,在国外是proxy.golang.org这个地址,可是在国内无法访问。
我们在国内需要使用如下的中央模块仓库地址:goproxy.cn
我们Go中的SDK默认是去找国外的中央模块仓库的,如何修改成国内的呢?
我们知道,所有的下载拉取行为脚本实际上是从 go download 这个脚本代码中实现的,而在这个脚本中的源码实现里,肯定有一个代码是写的是取出操作系统中的一个环境变量,这个环境变量存储着一个地址,这个地址代表了去哪个中央模块仓库拉取。
在GoSDK中的默认实现里,这个操作系统的环境变量叫做GOPROXY,在脚本中为其赋予了一个默认值,就是国外的proxy.golang.org这个值。
我们要想修改,只需要在当前电脑修改该环境变量的值即可:
(注意,这个变量值不带https,这只是一个变量,程序会自动拼接https)
windows
set GOPROXY=goproxy.cn
linux
export GOPROXY=goproxy.cn
3).切换到项目的根目录,也就是有go.mod的那层目录,打开命令行窗口。
执行 download指令(下载模块项目到${GOPATH}/pkg/mod下)
go mod download
4).如果不报错,代表已经下载好了,可以使用了,此时在项目根目录会生成一个go.sum文件。
一会再讲解sum文件。
5).此时可以进行开发了。
8.GoModule的真实使用场景2:
场景2:我们如何用命令创建一个Module的项目,(开发工具也能手动创建)。
切换到项目根目录,执行如下指令:
go mod init 模块名(模块名可不写)
然后会在根目录下生成一个go.mod文件
我们看看这个go.mod文件长啥样?
// 刚才init指令后的模块名参数被写在module后了
module 模块名
//表示使用GoSDK的哪个版本
go 1.14
修改go.mod文件中的依赖即可。
我们有两种方式下载和更新依赖:
1.修改go.mod文件,然后执行go mod down 把模块依赖下载到自己${GOPATH}/pkg/mod下,这里面装的是所有下载的module缓存依赖文件,其中有zip的包,也有源码,在一个项目文件夹下的不同文件夹下放着,还有版本号文件夹区分,每个版本都是一个文件夹。
2.直接在命令行使用go get package@version 更新或者下载依赖模块,升级或者降级模块的版本。(这里是开启模块后的go get指令用法)
例如:
go get github.com/gin-contrib/sessions@v0.0.1
这个指令执行过后,会自动修改go.mod中的文件内容,不需要我们手动修改go.mod文件中的内容。
9.go.mod文件详解
接下来我们讲讲核心配置文件go.mod
go.mod内容如下:
//表示本项目的module模块名称是什么,别的模块依赖此模块的时候写这个名字
module test
//表示使用GoSDK的哪个版本
go 1.14
//require中声明的是需要依赖的包和包版本号
require (
//格式如下: 需要import导入的模块名 版本号
// 需要import导入的模块名2 版本号2
// ... ...
github.com/gin-contrib/sessions v0.0.1
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.4.0
github.com/go-redis/redis v6.15.6+incompatible
github.com/go-sql-driver/mysql v1.4.1
github.com/golang/protobuf v1.3.2 // indirect
github.com/jinzhu/gorm v1.9.11
github.com/json-iterator/go v1.1.7 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/sirupsen/logrus v1.2.0
github.com/ugorji/go v1.1.7 // indirect
)
//replace写法如下,表示如果项目中有引入前面的依赖模块,改为引用=>后面的依赖模块,
//可以用于golang的国外地址访问改为指向国内的github地址,当然你在上面require直接写github就不用在这里repalce了
replace (
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a => github.com/golang/crypto v0.0.0-20190313024323-a1f597ede03a
)
//忽略依赖模块,表示在该项目中无论如何都使用不了该依赖模块,可以用于限制使用某个有bug版本的模块
exclude(
github.com/ugorji/go v1.1.7
)
注:go.mod 提供了module, require、replace和exclude四个命令
module语句指定包的名字(路径)
require语句指定的依赖项模块
replace语句可以替换依赖项模块
exclude语句可以忽略依赖项模块
上面github.com/ugorji/go v1.1.7 // indirect 有 indirect和非indirect
indirect代表此模块是间接引用的,中间隔了几个项目
这个不用特殊写,可以注释写便于识别和开发
10.GoModule有哪些命令?如何使用?
Go有如下关于Module的命令:
//go mod 命令:
download //下载依赖模块到${GOPATH}/pkg/mod
edit //一系列参数指令用于操作go.mod文件,参数太多,具体下面有例子
graph //输出显示每一个模块依赖了哪些模块
init //在一个非module项目的根目录下创建一个go.mod文件使其变为一个module管理的项目
tidy //根据项目实际使用的依赖包修改(删除和添加)go.mod中的文本内容
vendor //在项目根目录创建一个vender文件夹 然后把${GOPATH}/pkg/mod下载的本项目需要的依赖模块拷贝到本项目的vender目录下
verify //校验${GOPATH}/pkg/mod中的依赖模块下载到本地后是否被修改或者篡改过
why //一个说明文档的功能,用于说明一些包之间的为什么要这么依赖。(没啥用)
0). init和download
我们之前在案例中讲了init,download指令,这里不再赘述
1).go mod edit
是指在命令行用指令通过不同的参数修改go.mod文件,这个指令必须得写参数才能正确执行,不能空执行go mod edit
参数1 :-fmt
go mod edit -fmt
格式化go.mod文件,只是格式规范一下,不做其它任何内容上的修改。
其它任何edit指令执行完毕后都会自动执行-fmt格式化操作。
这个使用场景就是我们如果不想做任何操作,就想试试edit指令,就只需要跟上-fmt就行,因为单独不加任何参数
只有go mod edit后面不跟参数是无法执行的。
我们如何升级降级依赖模块的版本,或者说添加新的依赖和移除旧的依赖呢
参数2: -require=path@version / -droprequire=path flags
添加一个依赖
go mod edit -require=github.com/gin-contrib/sessions@v0.0.1
删除一个依赖
go mod edit -droprequire=github.com/gin-contrib/sessions@v0.0.1
这两个和go get package@version 功能差不多,但是官方文档更推荐使用go get来完成添加和修改依赖(go get 后的package和上面的path一个含义,都是模块全路径名)
参数3:-exclude=path@version and -dropexclude=path@version
排除某个版本某个模块的使用,必须有该模块才可以写这个进行排除。
go mod edit -exclude=github.com/gin-contrib/sessions@v0.0.1
删除排除
go mod edit -dropexclude=github.com/gin-contrib/sessions@v0.0.1
简单来说,执行这两个是为了我们在开发中避免使用到不应该使用的包
.....还有好几个,基本很少用,省略了
2).go mod graph
命令用法: 输出每一个模块依赖了哪些模块 无参数,直接使用 ,在项目根目录下命令行执行
go mod graph
比如:
模块1 依赖了模块a
模块1 依赖了模块b
模块1 依赖了模块c
模块2 依赖了模块x
模块2 依赖了模块z
如下是具体例子:
C:${GOPAHT}\file\project>go mod graph
file\project github.com/edgexfoundry/go-mod-bootstrap@v0.0.35
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/BurntSushi/toml@v0.3.1
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/edgexfoundry/go-mod-configuration@v0.0.3
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/edgexfoundry/go-mod-core-contracts@v0.1.34
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/edgexfoundry/go-mod-registry@v0.1.17
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/edgexfoundry/go-mod-secrets@v0.0.17
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/gorilla/mux@v1.7.1
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/pelletier/go-toml@v1.2.0
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/stretchr/testify@v1.5.1
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 gopkg.in/yaml.v2@v2.2.8
github.com/edgexfoundry/go-mod-configuration@v0.0.3 github.com/cenkalti/backoff@v2.2.1+incompatible
github.com/edgexfoundry/go-mod-configuration@v0.0.3 github.com/hashicorp/consul/api@v1.1.0
3).go mod tidy
根据实际项目使用到的依赖模块,在go.mod中添加或者删除文本引用
有一个参数可选项 -v 输出在go.mod文件中删除的引用模块信息
比如我们项目用到一个模块,go.mod中没写,执行后go.mod中就会添加上该模块的文本引用。
如果我们在go.mod中引用了一个模块,检测在真实项目中并没有使用,则会在go.mod中删除该文本引用。
使用如下:
go mod tidy -v
输出:
unused github.com/edgexfoundry/go-mod-bootstrap
输出表示检测项目没有使用到该模块,然后从go.mod中把该包的引用文字给删除了。
4).go mod vender
该指令会在项目中建立一个vender目录,然后把${GOPATG}/pkg/mod中下载的依赖拷贝到项目的vender目录中,方便管理和方便在idea中引用依赖。 -v参数可以在控制台输出相关的结果信息
go mod vender -v
5).go mod verify
验证下载到${GOPATH}/pkg/mod中的依赖模块有没有被修改或者篡改。
结果会输出是否被修改过
go mod verify
比如输出:
all modules verified
这个是所有模块已经验证,代表没有被修改,如果被修改,会提示哪些被修改。
6).go mod why
这个没啥用,说白了就是一个解释文档,输入参数和依赖他说明哪些包为啥要依赖这些包,不用看它,用处不大。
11.go.sum详细讲解
1).go.sum什么时候会更新或者新建生成?
当我们通过go mod download 下载完依赖模块或者go get package@version更新了依赖包的时候
,会检查根目录下有没有一个叫go.sum的文件,没有的话则创建一个并写入内容,有的话会更新go.sum中的内容。
2).go.sum是用来做什么的?
go.sum的作用是用来校验你下载的依赖模块是否是官方仓库提供的,对应的正确的版本的,并且中途没有被黑客篡改的。
go.sum主要是起安全作用和保证依赖的版本肯定是官方的提供的那个版本,版本确认具体是确认你下载的那个模块版本里面的代码的和官方提供的模块的那个版本的代码完全相同,一字不差。
通过go.sum保证安全性是很有必要的,因为如果你的电脑被黑客攻击了,黑客可以截取你对外发送的文件,也可以修改发送给你的文件,那么就会产生一个问题:
本来的路径应该是这样的: 第三方模块依赖库------------>你的电脑
结果中间有黑客会变成这样:
第三方模块依赖库-------->黑客修改了依赖库中的代码,植入病毒代码,并重新打成模块发送给你--------->你以为是官方的版本
结果黑客就把病毒代码植入到了你的项目中,你的项目就不安全了,面临着数据全部泄露的风险。
3).go.sum是如何实现校验机制的?它包含什么内容?
说到校验安全机制,有一种常规的玩法就是使用不可逆加密算法,不可逆加密算法是指将a文本通过算法加密成b文本后,b文本永远也不能反着计算出a文本。
不可加密算法的具体是怎么应用的呢?它是如何起作用的?
我们在这里先讲一个不可逆的加密算法SHA-256算法。
SHA-256算法的功能就是将一个任意长度的字符串转换成一个固定长度为64的字符串,比如:
4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce
这里从4e07代表四个字符串,按此算,这个加密后的字符串为64个。
为什么是64个呢?
因为64个字符串每两个字符为一组,比如4e是一组,07是一组,也就是说有32组,每一组是一个十六进制的数值,一个十六进制的数值也就是两个字符用计算机中的8个字节内存空间存储,也就是一个十六进制的数字,有两个字符串,占8个字节,一个字节等同8位(bit)(位只能存储0和1两个值),也就是说:
32(32个十六进制数,每个十六进制数用两个字符表示)*8字节=256位。
仔细看名字,SHA代表是算法的加密方式类型,256代表的是他这个是256位的版本。
具体原理实现是SHA内部定义了一系列固定数值的表,然后加密的时候无论是需要加密多少文字,它都按照一定的规则从需要加密的文字中按一定规则抽取其中的缩略一部分,然后拿缩略的一部分和SHA内部的固定数值表进行固定的hash映射和算术操作,这个hash映射和算术操作的顺序是固定写死的,公共数据表是写死的,这个写死的顺序和公共数据表就是这个算法的具体内容本质。
这样的话,因为抽取的是缩略的内容,所以我们可以把输出结果固定在64个字符,256位。
因为是缩略的内容, 所以我们不可能通过缩略的内容反推出完整的结果。
但是,相同的文本按照这个算法加密出来的64个字符肯定是相同的,同时,只要改变原需要加密文本的一个字符,也会造成加密出来的64个字符大不相同。
我们用SHA-256通常是这么用的:
A方 要 发送信息给 B方
B方 要确定信息是 A 方发送的,没有经过篡改
此时A和B同时约定一个密码字符串,比如abc。
这个abc只有A方和B方知道。
A 方把 需要传输的文本拼接上abc,然后通过SHA-256加密算出一个值,把原文本和算出的值全部发送给B。
B 方 拿出原文本,拼接上abc,进行SHA256计算,看看结果是否和传输过来的A传输的值一样,如果一样,代表中间没有被篡改。
为什么呢?
因为如果有一个黑客C想要篡改,他就得同时篡改原文本和算出的签名值。
可是C不知道密码是abc,它也就不能把abc拼接到原文后,所以它算出来的签名和B算出来的签名肯定不一致。
所以B如果自己算出的签名值与接收到的签名值不一致,B就知道不是A发过来的,就能校验发送端的源头是否是官方安全的了。
接下来我们讲一下go.sum的验证机制。
首先说下go.sum中存储的内容,这个文件存储的每一行都是如下格式
模块名 版本号 hash签名值
示例:
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
这里的hash签名值是拿当前模块当前版本号内的所有代码字符串计算出来的一个值,就是通过上面讲解的SHA-256计算的。
所以哪怕是这个模块中的代码有一个字变了,计算出来的hash值也不相同。
第三方模块库在每发布一个新的模块版本后,会按照SHA-256计算出对应版本的hash值,然后提供给外部获取用于检验安全性。
当我们 go mod download 和 go get package@version后 会更新go.mod中的模块路径和版本。
然后会更新或者创建根目录下go.sum文件中的模块名 版本号 和hash值。
在go.sum中的hash值是在下载和更新依赖包的时候,同时获取官方提供的版本号得来的。
也就是说,基本上go.sum中的文件都是从官网(外国)(中国是第三方模块仓库)上获得的正品版本号,这个版本号是仓库方自己算的,你只是获取到了存储到你自己的go.sum中。
具体如何获取版本号有个小知识点:
go module机制在下载和更新依赖的时候会取出操作系统中名为GOSUMDB的环境变量中的值,这个服务器地址值代表了从哪个第三方仓库获取对应的正品版本号。
重点来了,当你在go build 打包创建go项目的时候,go build的内部指令会去拿你本地的模块文件进行SHA-256计算,然后拿到一个计算出来的结果值,之后它会拿此值和go.sum中的正确的从官网拉取的值进行对比,如果不一样,说明这个模块包不是官方发布的,也就是你本地的模块包和官方发布的模块包中的代码肯定有差异。