赞
踩
为了解决 Golang 依赖问题,类似于 Rust 的 Cargo、Node.js 的 NPM、Python 的 Pip、Ruby 的 Boundler 等,Golang 最原始的依赖管理是 go get,执行命令后会拉取代码放入 GOPATH/src 下面,但是它是作为 GOPATH 下全局的依赖,并且 go get 还不能版本控制,以及隔离项目的包依赖。
对于以上这些问题,在go mod出现之前,有dep,govendor等包管理工具的出现,但都多多少少存在缺陷。
从 Go 1.11 版本开始,官方已内置了更为强大的 Go modules 来一统多年来 Go 包依赖管理混乱的局面(Go 官方之前推出的 dep 工具也几乎胎死腹中),并且将在 1.13 版本中正式默认开启,目前已受到社区的看好和强烈推荐,建议新项目采用 Go modules。作为新入局go的同学,可以跳过旧的包管理,直接了解和使用go modules机制(即go mod系列命令)来管理包。
golang提供了一个环境变量“GO111MODULE”,默认值为auto,如果当前目录里有 go.mod 文件,就使用 go modules,否则使用旧的 GOPATH 和 vendor 机制,因为在modules机制下go get只会下载go modules。
modules和传统GOPATH不同,不需要包含src,bin这样的子目录,一个源代码目录甚至是空目录都可以作为module,只要其中包含go.mod文件。
除了go.mod之外,go命令还维护一个名为go.sum的文件,其中包含特定模块版本内容的预期加密哈希,go命令使用go.sum文件确保这些模块的未来下载检索与第一次下载相同的位,以确保项目所依赖的模块不会出现意外更改,无论是出于恶意、意外还是其他原因。 go.mod和go.sum都应检入版本控制。
go.sum不需要手工维护,但是也可以了解一下,下面会进行介绍。
如果GO111MODULE=off,那么go命令行将不会使用新的module功能,相反的,它将会在vendor目录下和GOPATH目录中查找依赖包。也把这种模式叫GOPATH模式。
如果GO111MODULE=on,那么go命令行就会使用modules功能,而不会访问GOPATH。也把这种模式称作module-aware模式,这种模式下,GOPATH不再在build时扮演导入的角色,但是尽管如此,它还是承担着存储下载依赖包的角色。它会将依赖包放在GOPATH/pkg/mod目录下。
如果GO111MODULE=auto,这种模式是默认的模式,也就是说在你不设置的情况下,就是auto。这种情况下,go命令行会根据当前目录来决定是否启用module功能。只有当当前目录在GOPATH/src目录之外而且当前目录包含go.mod文件或者其子目录包含go.mod文件才会启用。
通俗的讲:
GO111MODULE=auto,即默认情况,当然=on与=off也包含在下述情况中:
一、没go.mod文件时,属于GOPATH模式,则使用 vendor 特性
二、有go.mod文件时,此时默认启用 modules特性
1.只找当前目录,不找GOPATH/src目录
2.当前目录下有vendor目录,则查找当前目录下vendor是否有此包;
3.当前目录下没有vendor目录,则查找GOROOT/src下是否有此包;
4.如果未找到,则启动GOPROXY特性,到仓库下载此包;
5.如果未下载到则提示包不存在;
包顺序:标准库 -> 项目内包 -> 内部第三方 -> 外部第三方包
原文参考:https://zhuanlan.zhihu.com/p/147568396,包括内容与图,特此声明。
GOPROXY控制Go Module下载的来源,有助于确保构建的确定性和安全性。
GOPROXY时代之前,在Golang开发时,模块依赖关系直接从版本控制(VCS)系统中的源存储库下载,如GitHub、Bitbucket、Bazaar、Mercurial或SVN。来自第三方的依赖项通常从公共源repos下载。这种形式缺乏确定性和安全性,以及开发中的两个基本需求:不变性和可用性。模块可以被作者删除,也可以编辑修改当前被发布的版本。
设置GOPROXY,将Go Module下载请求重定向到GOPROXY 指向的缓存库。使用GOPROXY进行模块依赖关系的管理的有助于开发构建不变性需求。另外GOPROXY的缓存还有助于确保模块始终可用,即使VCS repo中的原始模块已被销毁。
使用GOPROXY有不同的方法,这取决于你想使用的go模块依赖的来源,通常有公共的GOPROXY,私有Go Module,以及私有的GOPROXY。
公共GOPROXY是一个集中式的存储库,全球各地的Golang开发者都可以使用它。它缓存了大量开源的Go模块,这些模块可以从第三方公开访问的VCS项目存储库中获得。大多数此类GOPROXY,比如JFrog GoCenter,Goproxy.cn都是免费提供给Golang开发者社区的。此类GOPROXY 的架构拓扑如下图,提供了Go Module 的一致性以及可用性能力:
要使用公共GOPROXY,将Golang环境变量设置为其URL:
go env -w GOPROXY=https://goproxy.io
以上设置将所有模块下载请求重定向到GoCenter,从公共GOPROXY下载要比直接从VCS下载快得多。
除了完成下载之外,一个公共的GOPROXY还可以为Golang开发者提供关于它所拥有的模块的更详细的信息。JFrog GoCenter提供了丰富的UI,支持搜索和访问模块的安全信息(如cve)、非安全元数据(如Star数量,下载统计数据以及License信息)和gosumdb支持。这些元数据有助于用户在选择开源Go模块时做出更好的决策。
通常,Golang项目会同时使用开源和私有模块。一些用户使用GOPRIVATE环境变量来指定一个必须绕过GOPROXY和GOSUMDB的路径列表,并直接从VCS repos下载私有模块。例如,您可能希望使用GoCenter检索所有开源模块,但只从公司的服务器请求私有模块。如下图:
要使用GoCenter公共GOPROXY和私有模块,请设置Golang环境变量:
$ export GOPROXY=https://gocenter.io,direct
$ export GOPRIVATE=*.http://internal.mycompany.com
这种对GOPRIVATE的使用也确保了你对这些私有模块的使用不会因为请求到一个开放网络上的公共GOPROXY & checksum数据库服务器而“泄露”。
另一种替代方法是使用GONOSUMDB变量,该变量包含对私有go模块的引用。
虽然这种配置使Go客户端能够同时解析公共模块和私有模块依赖,但它并不强制私有模块的不可变性或可用性要求。
私有GOPROXY是一种在您自己的基础设施上存储公共和私有Go模块的工具。公共模块通过在二进制存储库管理器(如JFrog Artifactory)中代理一个公共GOPROXY缓存到企业内部网络。
私有模块也可以从VCS repos缓存到改存储库中。通过这种方式,可以保证公共和私有Go模块的不变性和可用性。在Artifactory中,您可以通过设置GoCenter的远程存储库(remote reposiroty),以及指向私有GitHub 仓库(用于私有模块)的远程Go模块存储库,以及本地Go模块存储库,将上述三个仓库组合到一个虚拟存储库中,作为用户统一单元进行访问,如下图:
在Artifactory中设置名为“go”的虚拟存储库的GOPROXY:
$ export GOPROXY="https://:@my.artifactory.server/artifactory/api/go/go
$ export GONOSUMDB="http://github.com/mycompany/*,http://github.com/mypersonal/*"
因为您的私有VCS repos中的模块在http://sum.golang.org的公共校验和数据库中没有条目,所以它们必须被排除在go客户端的检查之外。将GONOSUMDB设置为您的私有VCS repos可以实现这一点,并将防止这些私有模块的go get命令由于校验和不匹配而失败。
在这个配置中,您可以确保对私有模块的引用不会“泄漏”,同时还确保了公共模块和私有模块的不可变性和可用性。
综上,使用私有GOPROXY提供了最确定、最可靠和最安全的功能。您还可以通过您的私有GOPROXY到您的构建工具的网络接近度来加速模块依赖关系的解析。JFrog Artifactory可以安装在您最需要它的地方:本地数据中心部署或云中,或公共云提供商的SaaS版本。
这些好处不仅仅局限于Golang开发。大多数技术公司使用不止一种语言和多个包管理器。例如,如果代码是用Golang编写的,那么npm可能用于UI, Docker可能用于分发交付,Helm可能用于在k8上部署应用程序。通过支持超过27种包类型,Artifactory可以为所有应用程序提供确定性、稳定和安全的软件开发过程。
golang 配置 goproxy 几个可选的地址: 1.阿里云:https://mirrors.aliyun.com/goproxy 2.nexus社区提供的:https://gonexus.dev 3.goproxy.io:https://goproxy.io 4.基于athens的公共服务:https://athens.azurefd.net 5.官方提供的(jfrog 与 golang):https://gocenter.io 与 https://proxy.golang.org 6.七牛云赞助支持的:https://goproxy.cn 可参考网站:https://goproxy.io/zh/ 与 https://goproxy.cn/ Windows下配置命令(选一个即可):其他几个可以做为后备,以备不时之需。实际中,有时会出现某些库拉不下来,这时就需要换GOPROXY的配置 go env -w GOPROXY=https://goproxy.cn,direct go env -w GOPROXY=https://goproxy.io,direct go env -w GOPROXY=https://gocenter.io,direct(我正在使用这个,https://search.gocenter.io/可以在这个网站上查询各种Go Module Repository,很方便) ... // go env -w GOPRIVATE=*.corp.example.com # 设置不走 proxy 的私有仓库,多个用逗号相隔
module是一个相关Go包的集合,它是源代码更替和版本控制的单元。模块由源文件形成的go.mod文件的根目录定义,包含go.mod文件的目录也被称为模块根。moudles取代旧的的基于GOPATH方法来指定在工程中使用哪些源文件或导入包。模块路径是导入包的路径前缀,go.mod文件定义模块路径,并且列出了在项目构建过程中使用的特定版本。
go.mod文件定义module路径以及列出其他需要在build时引入的模块的特定的版本。例如下面的例子中,go.mod声明example.com/m路径时module的根目录,同时也声明了module依赖特定版本的golang.org/x/text和gopkg.in/yaml.v2。
module example.com/m
require (
golang.org/x/text v0.3.0
gopkg.in/yaml.v2 v2.1.0
)
go.mod文件还可以指定要替换和排除的版本,命令行会自动根据go.mod文件来维护需求声明中的版本。如果想获取更多的有关go.mod文件的介绍,可以使用命令go help go.mod。
go.mod文件用//注释,而不用/**/。文件的每行都有一条指令,由一个动作加上参数组成。例如:
module my/thing
require other/thing v1.0.2
require new/thing v2.3.4
exclude old/thing v1.2.3
replace bad/thing v1.4.5 => good/thing v1.4.5
上面三个动词require、exclude、replace分别表示:项目需要的依赖包及版本、排除某些包的特别版本、取代当前项目中的某些依赖包。
相同动作的命令可以放到一个动词+括号组成的结构中,例如:
require (
new/thing v2.3.4
old/thing v1.2.3
)
旧的版本,构建编译命令go build中的参数没有-mod参数,最新的版本现在多了这个,用来对go.mod文件进行更新或其他使用控制。形式如:go build -mod [mode],其中mode有以下几种取值:readonly,release,vendor。当执行go build -mod=vendor的时候,会在生成可执行文件的同时将项目的依赖包放到主模块的vendor目录下。
go get -m [packages]会将下载的依赖包放到GOPATH/pkg/mod目录下,并且将依赖写入到go.mod文件。go get -u=patch会更新主模块下的所有依赖包。
如果遇到不熟悉的导入包,任何可以查找包含该引入包模块的go命令,都会自动将该模块的最新版本添加到go.mod文件中。同时也会添加缺失的模块,以及删除无用的module。例如:go build, go test或者go list命令。另外,有一个专门的命令go mod tidy,用来查看和添加缺失的module需求声明以及移除不必要的。
go.mod文件是可读,也是可编辑的。go命令行会自动更新go.mod文件来维持一个标准格式以及精确的引入声明。
Go mod提供了一系列操作modules的命令,记住,所有的go命令中现在已经内置了对module的支持,而不仅仅是go mod命令。例如使用go get时,会经常自动在后台添加、移除、升级、降级依赖包版本。
命令语法:go mod [arguments]。Go mod提供的命令有下面几个,对于比较常用的命令进行详细说明。
download //下载模块到本地缓存,具体可以通过命令go env查看,其中环境变量GOCACHE就是缓存的地址,如果该文件夹的内容太大,可以通过命令go clean -cache
edit //从工具或脚本中编辑go.mod文件
graph //打印模块需求图
init //在当前目录下初始化新的模块
tidy //添加缺失的模块以及移除无用的模块
verify //验证依赖项是否达到预期的目的
why //解释为什么需要包或模块
用法:go mod download [-dir] [-json] [modules]。使用此命令来下载指定的模块,模块的格式可以根据主模块依赖的形式或者path@version形式指定。如果没有指定参数,此命令会将主模块下的所有依赖下载下来。
go mod download命令非常有用,主要用来预填充本地缓存或者计算Go模块代理的回答。默认情况下,下载错误会输出到标准输出,正常情况下没有任何输出。-json参数会以JSON的格式打印下载的模块对象,对应的Go对象结构是这样。
type Module struct {
Path string //module path
Version string //module version
Error string //error loading module
Info string //absolute path to cached .info file
GoMod string //absolute path to cached .mod file
Zip string //absolute path to cached .zip file
Dir string //absolute path to cached source root directory
Sum string //checksum for path, version (as in go.sum)
GoModSum string //checksum for go.mod (as in go.sum)
}
用法:go mod init [module]
。此命令会在当前目录中初始化和创建一个新的go.mod
文件,当然你也可以手动创建一个go.mod
文件,然后包含一些module
声明,这样就比较麻烦。go mod init
命令可以帮助我们自动创建,例如:
go mod init example.com/m
使用这条命令时,go.mod
文件必须提前不能存在。初始化会根据引入包声明来推测模块的路径或者如果你工程中之前已经存在一些依赖包管理工具,例如godep
,glide
或者dep
。那么go mod init
同样也会根据依赖包管理配置文件来推断。
默认情况下,Go不会移除go.mod文件中的无用依赖。所以当你的依赖中有些使用不到了,可以使用go mod tidy命令来清除它。
用法:go mod tidy [-v]它会添加缺失的模块以及移除不需要的模块。执行后会生成go.sum文件(模块下载条目)。添加参数-v,例如go mod tidy -v可以将执行的信息,即移除的模块打印到标准输出。
用法:go mod vendor [-v]
,此命令会将build
阶段需要的所有依赖包放到主模块所在的vendor
目录中,并且测试所有主模块的包。同理go mod vendor -v
会将添加到vendor
中的模块打印到标准输出。
用法:go mod verify
。此命令会检查当前模块的依赖是否已经存储在本地下载的源代码缓存中,以及检查自从下载下来是否有修改。如果所有的模块都没有修改,那么会打印all modules verified
,否则会打印变化的内容。
go.mod文件和go命令通常使用语义版本作为描述模块版本的标准形式,这样可以比较不同版本的先后顺序。例如模块的版本是v1.2.3,那么通过重新对版本号进行标签处理,得到该版本的虚拟版本。形式如:v0.0.0-yyyymmddhhmmss-abcdefabcdef。其中时间是提交时的UTC时间,最后的后缀是提交的哈希值前缀。时间部分确保两个虚拟版本号可以进行比较,以确定两者顺序。
下面有三种形式的虚拟版本号:
虚拟版本的生成不需要你去手动操作,go命令会将接收的commit哈希值自动转化为虚拟版本号。
上面我们说到,Go 在做依赖管理时会创建两个文件,go.mod
和 go.sum
。
相比于 go.mod
,关于 go.sum
的资料明显少得多。自然,go.mod
的重要性不言而喻,这个文件几乎提供了依赖版本的全部信息。而 go.sum
看上去就是 go module 构建出来的天书,而不是什么人类可读的数据。
但实际上,日常开发中我们仍然不得不跟 go.sum
打交道(通常是解决这个文件带来的合并冲突,抑或试图手工调整里面的内容)。如果不了解 go.sum
,只凭经验随便涂改,不一定能够改对。因此,为了更好地掌握 Go 的依赖管理,完全有必要了解 go.sum
的来龙去脉。
鉴于涉及 go.sum
的资料是如此地稀少(即使 Go 官方文档中,对于 go.sum
的描述也是支离破碎的),我花了些时间整理了相关的资料,希望读者可以从中受益。
go.sum
的每一行都是一个条目,大致是这样的格式:
<module> <version>/go.mod <hash>
或者
<module> <version> <hash>
<module> <version>/go.mod <hash>
其中module是依赖的路径,version是依赖的版本号。hash是以h1:
开头的字符串,表示生成checksum的算法是第一版的hash算法(sha256)。
有些项目实际上并没有 go.mod
这个文件,所以 Go 文档里提到这个 /go.mod
的 checksum,用了 “possibly synthesized” (也许是合成的)的说法。估计对于没有 go.mod
的项目,Go 会尝试生成一个可能的 go.mod
,并取它的 checksum。
如果只有对于 go.mod
的 checksum,那么可能是因为对应的依赖没有单独下载。比如用 vendor 管理起来的依赖,便只有 go.mod
的 checksum。
由于 go 的依赖管理背负着沉重的历史包袱,确定 version 的规则较为复杂。整个过程就像一个调查问卷,需要回答一个接一个的问题:
如果项目没有打 tag,会生成一个版本号,格式如下:
v0.0.0-commit日期-commitID
比如 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
。
引用一个项目的特定分支,比如 develop branch,也会生成类似的版本号:
v当前版本+1-commit日期-commitID
比如 github.com/DATA-DOG/go-sqlmock v1.3.4-0.20191205000432-012d92843b00 h1:Cnt/xQ9MO4BiAjZrVpl0BiqqtTJjXUkWhIqwuOCVtWo=
。
如果项目有用到 go module,那么就是正常地用 tag 来作为版本号。
比如 github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
。
如果项目打了 tag,但是没有用到 go module,为了跟用了 go module 的项目相区别,需要加个 +incompatible
的标志。
比如 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
关于 go module v2+ 的特性,可以参考 Go 的官方文档:https://blog.golang.org/v2-go…。简单而言,就是通过让依赖路径带版本号后缀来区分同一个项目里不同版本的依赖,类似于 gopkg.in/xxx.v2
的效果。
对于使用了 v2+ go module 的项目,项目路径会有个版本号的后缀。
比如 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
之所以 Go 会在依赖管理时引入 go.sum
这样的角色,是为了实现下面的目标:
(1)提供分布式环境下的包管理依赖内容校验
不像其他包管理机制,Go 采用分布式的方式来管理包。这意味着缺乏一个可供信赖的中心来校验每个包的一致性。
在主流的包管理机制中,通常存在一个中央仓库来保证每个发布的版本的内容不会被篡改。比如在 pypi 里面,即使发布过的版本存在严重的bug,发布者也不能重新发布一个同样版本,只能发布一个新版本。(但是却可以删掉已发布的版本抑或删掉整个项目,参考当年 npm 的 leftpad 事件,所以主流的包管理机制并非严格意义上的 Append Only。不过这并不影响我的论证)
而 Go 并没有一个中央仓库。发布者在 GitHub 上给自己的项目打上 0.1 的 tag 之后,依旧可以删掉这个 tag ,提交不同的内容后再重新打个 0.1 的 tag。哪怕发布者都是老实人,发布平台也可能作恶。所以只能在每个项目里存储自己依赖到的所有组件的 checksum,才能保证每个依赖不会被篡改。
(2)作为 transparent log 来加强安全性
go.sum 还有一个很特别的地方,就是它不仅仅记录了当前依赖的checksum,还保留了历史上每次依赖的 checksum。这种做法效法了 transparent log 的概念。transparent log 旨在维护一个 Append Only 的日志记录,提高篡改者的作案成本,同时方便审查哪些记录是篡改进来的。根据 Proposal: Secure the Public Go Module Ecosystem 的说法,go.sum 之所以要用 transparent log 的形式记录历史上的每个checksum,是为了便于 sum db 的工作。
不得不说的是,go.sum
也带来一些麻烦:
(1)容易产生合并冲突
这恐怕是 go.sum 最为人诟病的地方了。由于许多项目都没有通过打tag的方式来管理发布,每个commit都相当于新发布一个版本,这导致拉取它们的代码时会偶尔往 go.sum 文件里插入一条新记录。go.sum会记录间接依赖的特性,更是让这种情况雪上加霜。这一类的项目带来的影响可不小 —— 我粗略地统计下 go.sum 里这类记录的行数,大概占了总数的 40%。比如 golang.org/x/sys
在某个项目的 go.sum 里就有多达 37 个不同的版本。
如果只是莫名其妙的行数多,那最多不过是让人皱皱眉。在多人协作且用到几个经常升版本号的内部公共库的场景下,go.sum 会让人头疼。想象这种情况:
公共库原来有版本甲。
开发者A的分支a依赖了公共库版本乙,开发者B的分支b依赖了公共库版本丙。他们分别给 go.sum 添加记录如下:
common/lib 甲 h1:xxx
common/lib 乙 h1:yyy
common/lib 甲 h1:xxx
common/lib 丙 h1:zzz
之后公共库发布了版本丁,包含了版本乙和版本丙的功能。
然后合并分支a和分支b到主干,这时候就会有合并冲突。
现在有两个选择:
无论采用哪种方法,都需要手动介入。这无疑带来了不必要的工作量。
(2) 对于胡乱操作的第三方库,缺乏约束能力
go.sum 的本意在于提供防篡改的保障,如果拉第三方库的时候发现其实际内容和记录的校验值不同,就让构建过程报错退出。然而它能做的也就只限于此。go.sum 的检测功能,给库的使用者带来的负担更甚于库的开发者。在有中央仓库保障的其他包管理器里,人们可以在源头上限制那些捣蛋鬼,不让他们随意变更已经发布出去的版本。但是 go.sum 带来的约束纯粹是道德上的。如果一个库乱改已经发布的版本,会让依赖这个库的项目构建失败。对此库的使用者除了咒骂几句,在 issue 或别的地方痛斥作者,然后更新go.sum文件,似乎也没别的解决办法。犯错的本来是库的作者,麻烦的却是库的用户。这种设计可算不上高明。一个可能的解决办法是由官方把知名的库的各个版本镜像起来。虽然知名的库通常不会犯乱改已发布版本的错误,但是如果发生了(或者出于某种不可抗力发生了),至少有个镜像可用。然而这又回到单一中央仓库的路子上去。
(3) 实际情况下,手动编辑go.sum不可避免。比如前面举的,编辑go.sum文件解决合并冲突的情况。我也见过有些项目只在go.sum里保留依赖的最新版本的checksum。如果 go.sum 不是完全由工具管理的,又怎么能保证它一定是 Append Only 呢?如果 go.sum 不是 Append Only 的,又怎么能把它当作 transparent log 使用呢?
参考:
Go外部依赖包从vendor、 G O P A T H 和 GOPATH和 GOPATH和GOPATH/pkg/mod下的查找顺序
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。