阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

Go Modules 包管理工具的理解与使用

  • 2020-06-02
  • 本文字数:9943 字

    阅读完需:约 33 分钟

Go Modules 包管理工具的理解与使用

Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,Go1.14 上已经明确建议生产上使用了。而 Go modules 之前,Go 项目使用 GOPATH 模式,先讨论下 GOPATH 模式的问题。

GOPATH 的前世今生

GOPATH 是布置 Go 开发环境时所设置的一个环境变量。历史版本的 go 语言开发时,需要将代码放在 GOPATH 目录的 src 文件夹下。go get 命令获取依赖,也会自动下载到 GOPATH 的 src 下。


例如:


go get github.com/foo/bar会将代码下载到 $GOPATH/src/github.com/foo/bar
复制代码


可以通过命令 go env 获取当前设置的 GOPATH 路径。


例如我的电脑上设置如下:GOPATH="/Users/xxx/project/go"
复制代码


GOPATH 具体结构如下,必须包含三个文件夹,具体如下图所示:


GOPATH├── bin              //编译生成的二进制文件├── pkg              //预编译文件,以加快程序的后续编译速度|── src              //所有源代码    ├── github.com    ...    ...
复制代码

GOPATH 模式不再推荐的原因

最严重的问题就是,GOPATH 模式下,go get 命令使用时,没有版本选择机制,拉下来的依赖代码都会默认当前最新版本,而且如果当项目 A 和项目 B 分别依赖项目 C 的两个不兼容版本时, GOPATH 路径下只有一个版本的 C 将无法同时满足 A 和 B 的依赖需求。这可以说是一个很大的缺陷了,因而 Go1.13 起,官方就不再推荐使用 GOPATH 的模式了。

GOPATH 模式衍生出的版本管理工具的进化

随着 Go 语言使用人数的增长,依赖包的丰富,依赖版本问题尤其严重。


于是 Go 官方在 Go 1.5 的时候提出了实验性质的 vendor 机制:每个项目都可以有一个 vendor/ 目录来存放项目所需版本依赖的拷贝。


社区中基于官方给的机制,开发出了各种版本管理工具。比较流行的比如 govendor,以及之前曾被官方认定的 godep 工具等。


这些工具的思路基本都是为每个项目单独维护一份对应版本依赖的拷贝


管理工具虽然丰富了起来,但是不同版本工具之间不兼容,无法协作,各种工具还都有学习成本。这时候 在 Go 官方扶持下成立的 dep 项目被大家认为是未来一统江湖的版本管理工具,被称作 official experiment。


dep 采用了和 Rust 的管理工具 Cargo 类似的管理模式,原理在此不深究。


没过多久,Go 社区的核心人物 rsc 提出了 vgo 方案。一时间竟然出现了两个所谓的 Go 官方的版本管理方案。最终官方采用了 vgo 方案,随着 vgo 的逐渐成熟,Go 1.11 发布了该功能,并集成到了 Go 的官方工具中,也就是当前的 Go modules。

Go modules 相关环境变量

使用 Go modules,一般涉及到如下 6 个环境变量的配置,go env 可以列出,摘录如下:


GO111MODULE="auto"GOPROXY="https://goproxy.io,direct"GONOPROXY=""GOSUMDB="sum.golang.org"GONOSUMDB=""GOPRIVATE=""
复制代码

GO111MODULE

是开启使用 Go modules 的开关,可用参数值如下:


含义
auto在 GOPATH/src 之外,将自动使用 Go Modules 模式。否则还是用 GOPATH 模式。目前在最新的 Go1.14 中是默认值。
on启用 Go modules,将不使用 GOPATH,推荐设置,将会是未来版本中的默认值。
off 或者不设置Go 将使用 GOPATH 和沿用老的 vendor 机制,禁用 Go modules。不推荐设置。


设置开启 on 的方法


go env -w GO111MODULE=on
复制代码


也可在 .bash_profile 文件中直接环境变量方式设置,增加


export GO111MODULE=on
复制代码

GOPROXY

用于设置 Go 模块代理,其作用是使 Go 在拉取模块版本时直接通过镜像站点来快速拉取默认值是这个默认地址,国内无法访问,所以开启 Go Modules 必需设置镜像代理地址


列举国内常用的镜像代理地址如下:


地址简介
https://goproxy.io一个全球代理为 Go 模块而生
https://mirrors.aliyun.com/goproxy/阿里镜像代理
https://goproxy.cn七牛云赞助支持的


GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理


不使用,可设为 “off”


默认值中有个 direct,用来告诉 Go 将直接从依赖的源地址进行下载操作(比如 GitHub 等),例如当值列表中上一个 Go 代理返回 404 或 410 时,Go 自动尝试列表中的下一个,遇见 “direct” 时回到源地址去抓取。


最终:设置代理方法一般如下:如使用七牛网代理


go env -w GOPROXY=https://goproxy.cn,direct
复制代码


也可在 .bash_profile 文件中直接环境变量方式设置,增加


export GOPROXY=https://goproxy.cn,direct
复制代码

GOSUMDB

Go checksum database 的缩写,含义如其名字,用于在拉取模块版本时保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。


GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理,即设置 GOPROXY 自然就解决了。


若设置为“off”,就禁止 Go 在后续操作中校验模块版本。

GONOPROXY / GONOSUMDB / GOPRIVATE

如果当前项目依赖了私有模块,则配置会涉及这三个环境变量。例如公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。简单来说就是应对,GOPROXY 设置的代理或 GOSUMDB 设定的 Go checksum database 代理无法访问模块时的情形。


建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳设置是直接使用 GOPRIVATE。


它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如:


go env -w GOPRIVATE="*.my.com,github.com/eabc/def"
复制代码


设置后,前缀为 *.my.com 和 github.com/eabc/def 的模块都会被认为是私有模块。


将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 my.com 本身。

Go modules 操作命令及相关文件解读

可以命令行执行 go help mod 打印出 go mod 相关命令


download    download modules to local cache 常用,下载依赖包edit        edit go.mod from tools or scripts   ide编辑就行graph       print module requirement graph  查看使用而已init        initialize new module in current directory  常用tidy        add missing and remove unused modules   常用vendor      make vendored copy of dependencies  从mod cache中拷贝到项目的vendorverify      verify dependencies have expected contentwhy         explain why packages or modules are needed
复制代码


接下来主要介绍 go mod init 命令,及其涉及到的必要文件

go mod init 命令

初始化项目,使用方法例子如下:创建个空项目之后执行如下命令


go mod init gitee.com/biexiaoyansudian/my.cn
复制代码


指定了模块导入路径为 gitee.com/biexiaoyansudian/my.cn


这个 init 指定的路径作用是:


  • 作为模块的标识(identity)

  • 作为模块的 import path,当其他项目引用这个模块下的 package 时都会以该 import path 作为共同的前缀,比如:


import "gitee.com/biexiaoyansudian/my.cn/mypkg"
复制代码


go mod init 执行完毕,就初始化了使用 Go modules 的项目,会多出来一个 go.mod 文件。它记录了当前项目的模块信息,每一行都以一个关键词开头。

go.mod 文件

此时,go.mod 文件内容如下


module gitee.com/biexiaoyansudian/my.cn
go 1.14
复制代码


Go modules 模式下,使用 go get 命令,相关信息可以自动记录到 go.mod 文件中


看如下示例:


在刚才初始化的项目中,下载 gorm 扩展包


go get -u github.com/jinzhu/gorm
复制代码


默认下载最新版本,Go modules 模式下 go get 可以进行版本指定 @ 版本管理的 tag,例如


go get -u github.com/jinzhu/gorm@v1.0.0
复制代码


其拉取的结果缓存在 GOPATH/pkg/sumdb 目录下,而在 mod 目录下会以 github.com/foo/bar 的格式进行存放。


下载完成后,go.mod 文件内容自动变更如下


module gitee.com/biexiaoyansudian/my.cn
go 1.14
require github.com/jinzhu/gorm v1.9.12 // indirect
//手动注释 indirect 标识表示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,如果没引用,我们提前先拉下来这个包,就会出现该注释,比如直接使用go get拉代码包,而不是 go build 让命令自动根据 go.mod 拉代码包
复制代码


下面介绍下 go.mod 文件中可以使用到的语法关键词以及含义:


  • module: 定义当前项目的模块路径,不再赘述

  • go: 标识当前模块的 Go 语言版本,目前来看还只是个标识作用。

  • require: 说明 Module 需要什么版本的依赖。

  • exclude: 用于从使用中排除一个特定的模块版本。在实际的项目中很少被使用,故很少会显式的排除某个包的某个版本,除非我们知道某个版本有严重 bug。比如指令 exclude github.com/google/uuid v1.1.0,表示不使用 v1.1.0 版本。


场景举例:


当前项目 gitee.com/biexiaoyansudian/my.cn


下载依赖 go get github.com/google/uuid@v1.0.0


my.cn 的 go.mod 文件如下


module gitee.com/biexiaoyansudian/my.cn
go 1.14
require github.com/google/uuid v1.0.0 // indirect
复制代码


此时另创建项目 gitee.com/biexiaoyansudian/exclue


下载依赖 go get -uu github.com/google/uuid@v1.1.0


此时如果当前 my.cn 项目想要引入 exclue 项目,可以看到 exclue 项目用的是 uuid@v1.1.0 大于 my.cn 项目版本,则会自动的将 my.cn 项目的也升级为 uuid@v1.1.0


my.cn 的 go.mod 文件如下


module gitee.com/biexiaoyansudian/my.cn
go 1.14
require ( gitee.com/biexiaoyansudian/exclue v1.0.0 // indirect github.com/google/uuid v1.1.0)
复制代码


而如果在下载 exclue 依赖之前,在 my.cn 项目的 go.mod 文件里加上


exclude github.com/google/uuid v1.1.0
复制代码


此时,再去拉 exclue 项目,可以发现 uuid 直接跳过了 v1.1.0 版本升级到了 v1.1.1


my.cn 的 go.mod 文件如下


module gitee.com/biexiaoyansudian/my.cn
go 1.14
require ( gitee.com/biexiaoyansudian/exclue v1.0.0 // indirect github.com/google/uuid v1.1.1)
exclude github.com/google/uuid v1.1.0
复制代码

replace: 替换 require 中声明的依赖,使用另外的依赖及其版本号。

首先介绍个命令,可以查看项目最终所使用的全部依赖


go list -m all
复制代码

使用场景一:替换 require 的包

用例子来说明 replace 的使用场景一,一般也是最不会使用的场景。


当前项目的 go.mod 如下


module gitee.com/biexiaoyansudian/my.cn
go 1.14
require ( gitee.com/biexiaoyansudian/exclue v1.0.0 // indirect github.com/google/uuid v1.1.1)
exclude github.com/google/uuid v1.1.0
复制代码


下执行命令 go list -m all


gitee.com/biexiaoyansudian/my.cngitee.com/biexiaoyansudian/exclue v1.0.0github.com/google/uuid v1.1.1
复制代码


我们在 go.mod 下增加这样一句配置


replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
复制代码


再次执行命令,结果如下


gitee.com/biexiaoyansudian/my.cngitee.com/biexiaoyansudian/exclue v1.0.0github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
复制代码


发现最终生效的由 uuid v1.1.1 被替换成了 uuid v1.1.0


一般来说,这种场景使用的不多,和直接修改 require 作用相同


生效有前提条件:


一是只有在当前引用的模块有效


二是 replace 命令左侧的包名和版本,必须是 require 中包含的包名和版本

场景二:替换无法下载的包

由于网络问题,有些包无法下载,比如 golang.org 下的包,而这些包在 GitHub 都有镜像,此时就可以使用 GitHub 上的包来替换。


比如,项目中使用了 golang.org/x/text 包:


module github.com/renhongcai/gomodule
go 1.14
require ( github.com/google/uuid v1.1.1 golang.org/x/text v0.3.2)
replace github.com/google/uuid v1.1.1 => github.com/google/uuid v1.1.0
复制代码


项目编译时就会从 GitHub 下载包。我们源代码中 import 路径 golang.org/x/text/xxx 不需要改变

场景三:调试依赖包

我们调试依赖包,就可以使用 replace 来修改依赖,如下所示:


replace (    github.com/google/uuid v1.1.1 => ../uuid)
复制代码


使用本地的 uuid 来替换依赖包,此时,我们可以任意地修改 …/uuid 目录的内容来进行调试。


除了使用相对路径,还可以使用绝对路径,甚至还可以使用自已的 fork 仓库。

场景四:禁止被依赖

另一种使用 replace 的场景是你的 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 ...)
复制代码


由于上面的依赖都不存在 v0.0.0 版本,所以其他项目直接依赖 k8s.io/kubernetes 时会因无法找到版本而无法使用。


因为 Kubernetes 不希望作为 module 被直接使用,其他项目可以使用 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    ...)
复制代码

go.sum 文件

我们发现,在 go.mod 下还会生成个 go.sum 文件

用途

下载依赖包有可能被恶意篡改,以及缓存在本地的依赖包也有被篡改的可能,单单一个 go.mod 文件并不能保证一致性构建,Go 开发团队在引入 go.mod 的同时也引入了 go.sum 文件,用于记录每个依赖包的哈希值(SHA-256 算法),在 build 时,如果本地的依赖包 hash 值与 go.sum 文件中记录得不一致,则会拒绝 build。


示例:格式[/go.mod]


github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
复制代码


正常情况下,每个依赖包版本会包含两条记录,第一条记录为该依赖包版本整体(所有文件)的哈希值,第二条记录仅表示该依赖包版本中 go.mod 文件的哈希值,如果该依赖包版本没有 go.mod 文件,则只有第一条记录

go.sum 生成过程

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


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


在更新 go.sum 之前,为了确保下载的依赖包是真实可靠的,go 命令在下载完依赖包后还会查询 GOSUMDB 环境变量所指示的服务器,以得到一个权威的依赖包版本哈希值。如果 go 命令计算出的依赖包版本哈希值与 GOSUMDB 服务器给出的哈希值不一致,go 命令将拒绝向下执行,也不会更新 go.sum 文件。

go mod tidy

拉取缺少的模块,移除不用的模块

go mod vendor

将 GOPATH/src/pkg/mod 中的缓存包,复制到项目的 vendor 目录中,即使用每个项目使用自身包的模式,类似之前的 govendor 管理方式


尽管如此,默认情况下,go build 将忽略该目录的内容。如果想从 vendor/ 目录中建立依赖关系,需要如下参数 build


go build -mod vendor
复制代码

go mod download

下载依赖包

go mod edit

编辑 go.mod 文件一般直接用 ide 编辑就行

go mod graph

打印模块依赖图

go mod verify

验证依赖是否正确

go mod why

解释为什么需要依赖

补充与说明

关于自己发布包

go get 命令使用的版本是 git 中的 tag,我们制作包的时候也应该创建合适的 tag,例如


git tag v1.0.0git push --tags
复制代码


此时一个最佳实践是同时创建新分支 v1,以便后面我们修复错误和更新:


git checkout -b v1git push -u origin v1
复制代码


现在,我们可以使用 master 而不必担心破坏我们的版本。


接下来在 v1 分支中做修改,并将其标记为新版本。


git commit -m "修改"git tag v1.0.1git push --tags origin v1
复制代码


始终要保持在 v1 分支中修改,这个分支是始终的进行兼容更新的,如果有大的不兼容的改版,要新建并切换分支 v2,发布新的路径。

模块的使用与更新

我们可以在写代码的时候先写好导入的包等,之后初始化 go mod 模式,go build 或者 go mod tidy 等命令,会自动下载相关导入

小版本更新

go get -u 会更新最新的小版本,举个例子,它会将 1.0.0 更新为 1.0.1 或者 1.1.0 这样类似的版本。go get -u=patch 来获取最新的 patch 更新。 举个例子,它会更新到 1.0.1 而不是 1.1.0 版本。


例子总结:


由于我们的程序使用的是软件包的 1.0.0 版,并且我们刚刚创建了 1.0.1 版,以下任何 命令都会将我们更新到 1.0.1 版:


go get -ugo get -u=patchgo get github.com/robteix/testmod@v1.0.1
复制代码

大版本更新

根据语义版本语义,大版本不同于小版本。大版本会破坏向后兼容性。从 Go 模块的角度来看,大版本是一个完全不同的包。一个库的两个不兼容的版本,实质上是两个不同的库。其余的和以前一样,我们推它,把它标记为 v2.0.0 (并可选地创建一个 v2 分支。)


2020-06-02 10:0014249

评论 3 条评论

发布
用户头像
感谢
2021-12-23 17:07
回复
用户头像
写分非常好
2021-12-23 17:07
回复
用户头像
写的很完备。
2020-06-03 20:13
回复
没有更多了
发现更多内容

苹果电脑软件应用打开出现意外退出、崩溃问题解决办法

互联网搬砖工作者

OpenSea交易平台开发NFT系统部署技术

薇電13242772558

NFT

​ 如何处理Xcode上传IPA文件后无法在后台架构版本中显示的问题?

雪奈椰子

浪潮inBuilder低代码平台分布式微服务架构事务一致性技术解析

inBuilder低代码平台

分布式事务 低代码

智慧污水处理厂Web3D管理系统 把“污水洗白”

2D3D前端可视化开发

智慧水务 智慧污水厂 智慧污水处理 污水厂三维可视化 数字孪生污水厂

在生产环境中运行 grpc 服务所面临的挑战

黑客不够黑

golang gRPC

EasyMR 安全架构揭秘:如何管理 Hadoop 数据安全

袋鼠云数栈

大数据

EMQ&阿里云Lindorm联合方案:解决物联网关键业务场景数据处理难题

EMQ映云科技

阿里云 物联网 IoT 数据处理 企业号 4 月 PK 榜

软件团队文档管理工具哪个好?

PingCode

团队管理 PingCode 企业文档管理工具

新旧版本功能对比 | v1.5.0 全新升级

BinTools图尔兹

数据库 社区版 版本更新

阿里内部都在疯传!企业级Spring Boot 项目开发实战教程,我先肝了

程序知音

Java 微服务 springboot java架构 Java进阶

行业盛会丨九科信息亮相第十一届中国电子信息博览会(CITE2023),与您共享科技盛宴

九科Ninetech

GOPS 全球运维大会来了,龙蜥社区邀您一起了解“系统运维”

OpenAnolis小助手

开源 操作系统 内核 龙蜥社区 GOPS全球运维大会

面试还不懂Netty,看这篇文章就够了!

程序员小毕

Java 程序员 后端 Netty 架构师

NLP专栏简介:数据增强、智能标注、意图识别算法|多分类算法、文本信息抽取、多模态信息抽取、可解释性分析、性能调优、模型压缩算法等

汀丶人工智能

人工智能 自然语言处理 深度学习 关系抽取 命名实体识别

“ONE”有引力,4月21日见!

博睿数据

智能运维 博睿数据 发布会 Bonree ONE

上传了ipa但在苹果App Store中没有看到构建版本的问题

【福利】ChatGPT免费体验期延长,商用版正式开启预约!

WorkPlus

低代码是行业毒瘤吗?

这我可不懂

团队 低代码 JNPF 专业低代码

PCB拼版对SMT组装的影响,华秋一文告诉你

华秋电子

华秋PCB生产工艺分享 | 第十一道之成型

华秋电子

面试官:谈谈分布式一致性机制

Java永远的神

程序员 分布式 微服务 后端 架构师

关于FTP文件传输协议说明,带你了解更详细的文件传输协议

镭速

携多款产品亮相“深圳先进制造业集群展”,华秋积极探索发展机遇

华秋电子

选择低代码,帮企业数字化转型提交一份满分答卷

加入高科技仿生人

软件开发 低代码 数字化 数字转型

折叠屏时代的全新故事,由华为领启

脑极体

华为

MobTech ShareSDK|分享报错怎么办

MobTech袤博科技

现在学C4D还是Blender好?这俩有啥区别?

Finovy Cloud

blender C4D

关于 IDP 的五大认知误解

SEAL安全

IdP 平台工程

智慧公共厕所,重构城市公共服务的新模式@光明源智慧公厕

光明源智慧厕所

智慧城市

【论文解读】不和谐区域定位

合合技术团队

人工智能 图像识别 图像处理

Go Modules 包管理工具的理解与使用_架构_尚新鹏_InfoQ精选文章