go mod机制详解

go mod 机制详解

官方的定义:

A module is a collection of related Go packages that are versioned together as a single unit. 一组package的集合,一起被标记版本,即是一个module

一个项目中可以包含多个package,不管有多少个package,这个package都将随项目一起发布

一个module的版本号规则必须遵循语义化规范(https://semver.org/)版本号必须使用格式v(major).(minor).(patch),比如v0.1.0v1.2.3 或v1.5.0-rc.1

一, 初始化项目

#cd project     
#go mod init 

Go module实际上只是精准的记录项目的依赖情况,包括每个依赖的精确版本号生成有描述依赖的go.mod和记录module的checksum的go.sum.

go.mod的内容:

  • 项目名
  • go 的版本
  • 第三方的包

开始编译前:

#go get

注意开启go mod 模式后 go get 下载的包都在 go/pkg 目录下

将自动下载依赖包并解压,go get 总是获取最新版本的包
如果包的版本更新了,go get 命令将自动修改go.mod文件
go.sum 该文件记录了所依赖包的hash值,确保依赖包没有被篡改

注意:
 经go get 修改的go.mod和go.sum都需要提交的代码库,
 这样别人获取项目代码并编译时 就会使用项目所要求的以来版本。

如果你没有使用 go get 下载依赖而是直接使用 go build main.go 运行项目,依赖包也会自动下载。但是go 的v1.13.4有个bug,此时生成的go.mod显示的依赖显示

require google.golang.org/protobuf v1.1.1 // indirect

注意行末的indirect表示间接依赖,这明显是错误的,因为我们直接import的。

二,go mod 的指令

go.mod文件中通过指令声明module信息,用于控制命令行工具进行版本选择。一共有四个指令可供使用:

 module:  声明module名称;
 require: 声明依赖以及其版本号;
 replace: 替换require中声明的依赖,使用另外的依赖及其版本号;
 exclude: 禁用指定的依赖;

1. module

#go.mod文件首行
module github.com/jk/test  //指定项目名

2. require

#该指令告诉go build 使用该版本的包来编译
require (    
	google.golang.org/protobuf v1.1.1
)

3. replace 工作机制

google.golang.org/protobuf v1.1.1

如果我们想使用protobuf的v1.1.0版本进行构建,可以修改require指定的版本号,还可以使用replace来指定

正常情况下是不需要用replace的,这不是它的使用场景,下面会有使用场景

[root@ecs-d8b6]# cat go.mod 
module github.com/jk/test
go 1.13
require google.golang.org/protobuf v1.1.1
replace google.golang.org/protobuf v1.1.1 => google.golang.org/protobuf v1.1.0
#此时编译时就会选择v1.1.0 版本,如果没有会自动下载

replace 的使用场景

  • 替换无法下载的包

大陆网络问题有些包无法下载 比如golang.org但是
可以从github.com clone下来

replace (    
golang.org/x/text v0.3.2 => github.com/golang/text v0.3.2
)
  • 禁止被依赖

你的module不希望被直接引用,比如开源软件kubernetes,在它的go.mod中require部分有大量的v0.0.0依赖

module k8s.io/kubernetes
require (    
  ...    
  k8s.io/api v0.0.0   
  k8s.io/apiextensions-apiserver v0.0.0   
  k8s.io/apimachinery v0.0.0    
  k8s.io/client-go v0.0.0    
  k8s.io/cloud-provider v0.0.0    
  ...
)

上面的依赖都不存在v0.0.0版本,所以其他项目直接依赖k8s.io/kubernetes时会因无法找到版本而无法使用
kubernetes 对外隐藏了依赖版本号,其真实的依赖通过replace指定

replace (
    k8s.io/api => ./staging/src/k8s.io/api    
    k8s.io/apiextensions-apiserver => ./staging/src/
    k8s.io/apiextensions-apiserver       
    k8s.io/apimachinery => ./staging/src/k8s.io/apimachinery    
    k8s.io/apiserver => ./staging/src/k8s.io/apiserver    
    k8s.io/cli-runtime => ./staging/src/k8s.io/cli-runtime    
    k8s.io/client-go => ./staging/src/k8s.io/client-go    
    k8s.io/cloud-provider => ./staging/src/k8s.io/cloud-provider
)

注意:
replace指令在当前模块不是main module时会被自动忽略的,Kubernetes正是利用了这一特性来实现对外隐藏依赖版本号来实现禁止直接引用的目的

4. exclude指令

go.mod文件中的exclude指令用于排除某个包的特定版本

与replace类似,也仅在当前module为main module时有效,其他项目引用当前项目时,exclude指令会被忽略

exclude指令在实际的项目中很少被使用,因为很少会显式地排除某个包的某个版本,除非我们知道某个版本有严重bug

比如指令:

#表示不使用v1.1.0
exclude github.com/google/uuid v1.1.0

三,go.mod文件中关键字的意思

indirect的含义

这个标识总是出现在require指令中,其中//与代码的行注释一样表示注释的开始,indirect表示间接的依赖

例如.k8s 的v1.17.0版本 go.mod 文件中就有数十个依赖包被标记为indirect

require (
    github.com/Rican7/retry v0.1.0 // indirect    
    github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7 // indirect    
    github.com/boltdb/bolt v1.3.1 // indirect    
    github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b // indirect    
    github.com/codegangsta/negroni v1.0.0 // indirect    
    ...
)

在执行命令go mod tidy时,Go module 会自动整理go.mod 文件

如果有必要会在部分依赖包的后面增加// indirect注释。

一般而言,被添加注释的包肯定是间接依赖的包,而没有添加// indirect注释的包则是直接依赖的包,即明确的出现在某个import语句中。

然而,并不是所有的间接依赖都会出现在 go.mod文件中。
间接依赖出现在go.mod文件的情况,可能符合下面所列场景的一种或多种:

  • 直接依赖未启用 Go module
  • 直接依赖go.mod 文件中缺失部分依赖

1. 直接依赖未启用 Go module的包

Module A 依赖 B,但是 B 还未切换成 Module,即没有go.mod文件

此时,当使用go mod tidy命令更新A的go.mod文件时,B的两个依赖B1和B2将会被添加到A的go.mod文件中

A --> B {B1,B2}

此时Module A的go.mod文件中require部分将会变成

#依赖B及B的依赖B1和B2都会出现在go.mod文件中
require (
    B vx.x.x   
    B1 vx.x.x // indirect    
    B2 vx.x.x // indirect
)

2. 直接依赖的包的 go.mod 文件不完整

上述,如果依赖B没有go.mod文件,则Module A 将会把B的所有依赖记录到A 的go.mod文件中。

即便B拥有go.mod.如果go.mod文件不完整的话,Module A依然会记录部分B的依赖到go.mod文件中

Module B虽然提供了go.mod文件中,但go.mod文件中只添加了依赖B1,那么此时A在引用B时,则会在A的go.mod文件中添加B2作为间接依赖,B1则不会出现在A的go.mod文件中。

 A --> B {B1,B2}

此时Module A的go.mod文件中require部分将会变成

#由于B1已经包含进B的go.mod文件中,A的go.mod文件则不必再记录
#会记录缺失的B2

require (
    B vx.x.x    
    B2 vx.x.x // indirect
)

四,版本选择机制

使用go get 获取依赖若不指定版本就会拉取最新的,
如果本地有go.mod会自动更新。

事实上 go get ,go build ,go mod tidy 也会自动选择依赖版本,这些依赖选择都会遵循一些规则。

1. 依赖包版本约定

在Go module时代,module版本号要遵循语义化版本规范,    
即版本号格式为v<major>.<minor>.<patch>,如v1.2.3

当有不兼容的改变时,需要增加major版本号,如v2.1.0。Go module规定,如果major版本号大于1,则major版本号需要显式地标记在module名字中.

如:module github.com/my/mod/v2

这样做的好处是Go module 会把module github.com/my/mod/v2module github.com/my/mod视做两个module,他们甚至可以被同时引用

2. 版本选择机制

当在源代码中增加了新的import,go build ,go test 这些命令将会自动选择一个最优的版本,并更新go.mod文件

如果go.mod文件中已标记了某个依赖包的版本号,则这些命令不会主动更新go.mod中的版本号

所谓自动更新版本号只在go.mod中缺失某些依赖或者依赖不匹配时才会发生

3. 最小版本选择

有时记录在go.mod文件中的依赖包版本会随着引入其他依赖包而发生变化。

commit-A 对应的tag版本为 v1.1.1 ->commit-B 没有tag版本

Module A 依赖 Module M的v1.0.0版本,但之后 Module A 引入了 Module D,而Module D 依赖 Module M的v1.1.1版本

此时,由于依赖的传递,Module A也会选择v1.1.1版本。

需要注意的是,此时会自动选择最小可用的版本,而不是最新的tag版本

五,incompatible 机制

前面我们介绍了Go module的版本选择机制,其中介绍了一个Module的版本号需要遵循v..的格式,

此外,如果major版本号大于1时,其版本号还需要体现在Module名字中。

比如Module github.com/RainbowMango/m,如果其版本号增长到v2.x.x时,
其Module名字也需要相应的改变为:github.com/RainbowMango/m/v2
即,如果major版本号大于1时,需要在Module名字中体现版本

问题来了:

如果Module的major版本号虽然变成了v2.x.x,但Module名字仍保持原样会怎么样呢? 其他项目是否还可以引用呢?其他项目引用时有没有风险呢?

1. 能否引用不兼容的包

以Module github.com/RainbowMango/m 为例假如其当前版本为v3.6.0,因为其Module名字未遵循Golang所推荐的风格,即Module名中附带版本信息,我们称这个Module为不规范的Module。

不规范的Module还是可以引用的,但跟引用规范的Module略有差别

如果我们在项目A中引用了该module,使用命令go mod tidy,go 命令会自动查找Module 的最新版本,即v3.6.0。
由于Module为不规范的Module,为了加以区分,go 命令会在go.mod中增加+incompatible 标识

require (
    github.com/RainbowMango/m v3.6.0+incompatible
)

+incompatible除了增加不兼容标识,其他没有本质的区别

2. 如何处理 +incompatible

出现+incompatible,说明你引用了一个不规范的Module,正常情况下,只能说明这个Module版本未遵循版本化语义规范。引用这个会有一定的风险。

比如,我们拿某开源Module github.com/blang/semver为例,编写本文时,该Module最新版本为v3.6.0,但其go.mod中记录的Module却是:

module github.com/blang/semver

Module github.com/blang/semver 在k8s中被引用,那么K8s的go.mod文件则会标记这个Module为+incompatible

require (
    ...    
    github.com/blang/semver v3.5.0+incompatible    
    ...
 )

站在K8s的角度:

如果将来 github.com/blang/semver发布了新版本v4.0.0,Module 名字然为github.com/blang/semver。升级这个Module的版本将会变得困难。

因为v3.6.0到v4.0.0跨越了大版本,按照语义化版本规范来解释说明发生了不兼容的改变,即然不兼容,项目维护者有必须对升级持谨慎态度,甚至放弃升级。

站在github.com/blang/semver的角度:

如果不能将自身变得”规范”,那么其他项目有可能放弃本Module,转而使用其他更规范的Module来替代,开源项目如果没有使用者,也就走到了尽头

六, 什么是伪版本?

go.mod中通常使用语义化版本来标记依赖,比如v1.2.3、v0.1.5等。
诸如v1.2.3和v0.1.5这样的语义化版本,实际是某个commit ID的标记,
真正的版本还是commit ID。

比如:

github.com/renhongcai/gomodule 项目的v1.5.0对应的真实版本为:20e9757b072283e5f57be41405fe7aaf867db220

哪当实际开发中,不得不引用一个最新的commit id时怎么办?

比如:

某项目发布了v1.5.0版本,但随即又修复了一个bug(引入一个新的commit ID),而且没有发布新的版本。此时,如果我们希望使用最新的版本,就需要直接引用最新的commit ID

使用commit ID的版本在Go语言中称为pseudo-version,可译为”伪版本伪版本的版本号通常会使用vx.y.z-yyyymmddhhmmss-abcdefabcdef格式 vx.y.z看上去像是一个真实的语义化版本,但通常并不存在该版本,所以称为伪版本。另外abcdefabcdef表示某个commit ID的前12位

require (
    go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738
  )

1. 如何使用伪版本

在仓库github.com/renhongcai/gomodule中存在v1.5.0 tag 版本,在v1.5.0之后又提交了一个commit,并没有发布新的版本

commit-A 对应的tag版本为 v1.5.0-> commit-B 没有tag

如果我们要使用commit-A,即v1.5.0:

#go get github.com/renhongcai/gomodule@v1.5.0

如果我们要使用commit-B:

go get github.com/renhongcai/gomodule@6eb27062747a458a27fb05fceff6e3175e5eca95

此时 go.mod 就出现了伪版本

七,依赖包的存储

GOPATH模式下,依赖包存储在$GOPATH/src,该目录下只保存特定依赖包的一个版本。

而在GOMODULE模式下,依赖包存储在$GOPATH/pkg/mod,该目录中可以存储特定依赖包的多个版本(go get 指定下载多个版本)

$GOPATH/pkg/mod目录下有个cache目录,它用来存储依赖包的缓存,简单说,go命令每次下载新的依赖包都会在该cache目录中保存一份

八, go.sum 文件

引入原由:
为了确保一致性构建,Go引入了go.mod文件来标记每个依赖包的版本,在构建过程中go命令会下载go.mod中的依赖包,下载的依赖包会缓存在本地,以便下次构建。

考虑到下载的依赖包有可能是被黑客恶意篡改的,以及缓存在本地的依赖包也有被篡改的可能,单单一个go.mod文件并不能保证一致性构建

为了解决Go module的这一安全隐患,Go开发团队在引入go.mod的同时也引入了go.sum文件,用于记录每个依赖包的哈希值

在构建时,如果本地的依赖包hash值与go.sum文件中记录得不一致,则会拒绝构建。

1. go.sum文件记录

go.sum文件中每行记录由module名、版本和哈希组成,并由空格分开:

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

比如:
go.sum文件中记录了github.com/google/uuid 这个依赖包的v1.1.1版本的哈希值:

github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=  
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGEw/LqOeaOT+nhxU+yHo=

在Go module机制下,我们需要同时使用依赖包的名称和版本才可以准确的描述一个依赖
每个依赖包版本会包含两条记录:

 第一条记录为该依赖包版本整体(所有文件)的哈希值
 第二条记录仅表示该依赖包版本中go.mod文件的哈希值

如果该依赖包版本没有go.mod文件,则只有第一条记录。如上面的例子中,v1.1.1表示该依赖包版本整体,而v1.1.1/go.mod表示该依赖包版本中go.mod文件

依赖包版本中任何一个文件(包括go.mod)改动,都会改变其整体哈希值,此处再额外记录依赖包版本的go.mod文件主要用于计算依赖树时不必下载完整的依赖包版本,只根据go.mod即可计算依赖树

2. go sum如何生成

go get命令首先会将该依赖包下载到本地缓存目录$GOPATH/pkg/mod/cache/download
该依赖包为一个后缀为.zip的压缩包,如v1.0.0.zip。go get下载完成后会对该.zip包做哈希运算,并将结果存放在后缀为.ziphash的文件中,如v1.0.0.ziphash。

如果在项目的根目录中执行go get命令的话,go get会同步更新go.mod和go.sum文件,go.mod中记录的是依赖名及其版本,

比如:

require (
    github.com/google/uuid v1.0.0
)

go.sum文件中则会记录依赖包的哈希值
同时还有依赖包中go.mod的哈希值

比如:

github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGEw/LqOeaOT+nhxU+yHo=

在更新go.sum之前,为了确保下载的依赖包是真实可靠的,go命令在下载完依赖包后还会查询GOSUMDB环境变量所指示的服务器,以得到一个权威的依赖包版本哈希值。

如果go命令计算出的依赖包版本哈希值与GOSUMDB服务器给出的哈希值不一致,go命令将拒绝向下执行,也不会更新go.sum文件

3. go.sum 校验

当我们拉下项目的源代码在本地编译构建时,go命令会从本地缓存中查找所有go.mod中记录的依赖包,并计算本地依赖包的哈希值,然后与go.sum中的记录进行对比,即检测本地缓存中使用的依赖包版本是否满足项目go.sum文件的需求。

如果校验失败,说明本地缓存目录中依赖包版本的哈希值和项目中go.sum中记录的哈希值不一致,go命令将拒绝构建。

这就是go.sum存在的意义,即如果不使用我期望的版本,就不能构建。
当校验失败时,有必要确认到底是本地缓存错了,还是go.sum记录错了。

二者都可能出错:本地缓存目录中的依赖包版本有可能被有意或无意地修改过,项目中go.sum中记录的哈希值也可能被篡改过

当校验失败时,go命令倾向于相信go.sum,因为一个新的依赖包版本在被添加到go.sum前是经过GOSUMDB(校验和数据库)验证过的。此时即便系统中配置了GOSUMDB(校验和数据库),go命令也不会查询该数据库.

4. 校验和数据库

环境变量GOSUMDB标识一个checksum database,即校验和数据库,实际上是一个web服务器,该服务器提供查询依赖包版本哈希值的服务。

该数据库中记录了很多依赖包版本的哈希值,比如Google官方的sum.golang.org则记录了所有的可公开获得的依赖包版本。除了使用官方的数据库,还可以指定自行搭建的数据库(自己的vendor),甚至干脆禁用它(export GOSUMDB=off)

如果系统配置了GOSUMDB,在依赖包版本被写入go.sum之前会向该数据库查询该依赖包版本的哈希值进行二次校验,校验无误后再写入go.sum

如果系统禁用了GOSUMDB,在依赖包版本被写入go.sum之前则不会进行二次校验,go命令会相信所有下载到的依赖包,并把其哈希值记录到go.sum

以上 get !

扩展

Go 1.11之后推出了依赖包管理工具Go Modules,
Go项目可以在 GOPATH 之外的位置创建,当项目中仅使用了公有库作为依赖时,使用 go get 或 go mod 更新依赖。由于Go Modules默认使用代理去更新依赖,所以当使用了私有仓库作为依赖时,Go更新依赖的相关命令将不再可用。

比如自己的两个项目,A项目,B项目,而B项目中引用了A中的数据

  1. 解决办法:在B项目的go.mod中添加
module B

go 1.15

require (
	bmc-common v0.0.0-00010101000000-000000000000 //自动生成
	cloud_server v0.0.0-00010101000000-000000000000//自动生成
	google.golang.org/grpc v1.45.0
	google.golang.org/protobuf v1.28.0
)

replace bmc-common => ../bmc-common    //bmc-common 就是项目A
replace cloud_server => ../cloud_server  //bmc-common 就是项目A
package model //B项目中

import (
	"bmc-common/dao" //就可以直接使用A中的包了
	"strings"
)

2.解决办法:将引用的私有包维护在云端,像公有包一样使用

自建私有仓库, 通常不用 http/https 拉代码的, 使用 ssh 的方式. 首先我们会配置 ssh key, 免密操作 git.在gitlab 创建cloud_package,

假设 git 仓库的域名是 gitlab.xxx.com, 使用的是 https 协议, go mod 是 gitlab.xxx.com/aa/module_name
export GO111MODULE=on
export GOPROXY=https://goproxy.io,direct
export GOPRIVATE=gitlab.xxx.net

因为go get 命令的project地址都是https协议的,这个在download public repository时没有问题,但是对于private repository则不行,我们需要加一个git配置

Git请求的认证
git config --global url.“git@gitlab.xxx.net”.insteadOf “https://gitlab.xxx.net”
go get gitlab.xxx.net/cloud_package/cloud_package

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340