前言

通过学习Go是怎么解决包依赖管理问题的?go module基本使用,我们掌握了 Go Module 构建模式的基本概念和工作原理,也初步学会了如何通过 go mod 命令,将一个 Go 项目转变为一个 Go Module,并通过 Go Module 构建模式进行构建。

但是,围绕一个 Go Module,Go 开发人员每天要执行很多 Go 命令对其进行维护。这些维护又是怎么进行的呢?

具体来说,维护 Go Module 无非就是对 Go Module 依赖包的管理。但在具体工作中还有很多情况,接下来会拆分成六个场景,层层深入给你分析。可以说,学好这些是每个 Go 开发人员成长的必经之路。

为当前 module 添加一个依赖

在一个项目的初始阶段,我们会经常为项目引入第三方包,并借助这些包完成特定功能。即便是项目进入了稳定阶段,随着项目的演进,我们偶尔还需要在代码中引入新的第三方包。

那么我们如何为一个 Go Module 添加一个新的依赖包呢?

我们还是以之前的例子中讲过的 module-mode 项目为例。如果我们要为这个项目增加一个新依赖:github.com/google/uuid,那需要怎么做呢?

我们首先会更新源码,就像下面代码中这样:

package main

import (
"github.com/google/uuid"
"github.com/sirupsen/logrus"
) func main() {
logrus.Println("hello, go module mode")
logrus.Println(uuid.NewString())
}

新源码中,我们通过 import 语句导入了 github.com/google/uuid,并在 main 函数中调用了 uuid 包的函数 NewString。此时,如果我们直接构建这个 module,我们会得到一个错误提示:

$ go build
main.go:4:2: no required module provides package github.com/google/uuid; to add it:
go get github.com/google/uuid

Go 编译器提示我们,go.mod 里的 require 段中,没有哪个 module 提供了 github.com/google/uuid 包,如果我们要增加这个依赖,可以手动执行 go get 命令。那我们就来按照提示手工执行一下这个命令:

$ go get github.com/google/uuid
go: downloading github.com/google/uuid v1.3.0
go get: added github.com/google/uuid v1.3.0

你会发现,go get 命令将我们新增的依赖包下载到了本地 module 缓存里,并在 go.mod 文件的 require 段中新增了一行内容:

require (
github.com/google/uuid v1.3.0 //新增的依赖
github.com/sirupsen/logrus v1.8.1
)

这新增的一行表明,我们当前项目依赖的是 uuid 的 v1.3.0 版本。我们也可以使用 go mod tidy 命令,在执行构建前自动分析源码中的依赖变化,识别新增依赖项并下载它们:

$ go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.3.0

对于我们这个例子而言,手工执行 go get 新增依赖项,和执行 go mod tidy 自动分析和下载依赖项的最终效果,是等价的。但对于复杂的项目变更而言,逐一手工添加依赖项显然很没有效率,go mod tidy 是更佳的选择。

到这里,我们已经了解了怎么为当前的 module 添加一个新的依赖。但是在日常开发场景中,我们需要对依赖的版本进行更改。那这又要怎么做呢?下面我们就来看看下面升、降级修改依赖版本的场景。

升级 / 降级依赖的版本

我们先以对依赖的版本进行降级为例,分析一下。

在实际开发工作中,如果我们认为 Go 命令自动帮我们确定的某个依赖的版本存在一些问题,比如,引入了不必要复杂性导致可靠性下降、性能回退等等,我们可以手工将它降级为之前发布的某个兼容版本。

那这个操作依赖于什么原理呢?

答案就是我们之前说过的“语义导入版本”机制。我们再来简单复习一下,Go Module 的版本号采用了语义版本规范,也就是版本号使用 vX.Y.Z 的格式。其中 X 是主版本号,Y 为次版本号 (minor),Z 为补丁版本号 (patch)。主版本号相同的两个版本,较新的版本是兼容旧版本的。如果主版本号不同,那么两个版本是不兼容的。

有了语义版本号作为基础和前提,我们就可以从容地手工对依赖的版本进行升降级了,Go 命令也可以根据版本兼容性,自动选择出合适的依赖版本了。

我们还是以上面提到过的 logrus 为例,logrus 现在就存在着多个发布版本,我们可以通过下面命令来进行查询:

$ go list -m -versions github.com/sirupsen/logrus
github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1

在这个例子中,基于初始状态执行的 go mod tidy 命令,帮我们选择了 logrus 的最新发布版本 v1.8.1。如果你觉得这个版本存在某些问题,想将 logrus 版本降至某个之前发布的兼容版本,比如 v1.7.0,那么我们可以在项目的 module 根目录下,执行带有版本号的 go get 命令

$ go get github.com/sirupsen/logrus@v1.7.0
go: downloading github.com/sirupsen/logrus v1.7.0
go get: downgraded github.com/sirupsen/logrus v1.8.1 => v1.7.0

从这个执行输出的结果,我们可以看到,go get 命令下载了 logrus v1.7.0 版本,并将 go.mod 中对 logrus 的依赖版本从 v1.8.1 降至 v1.7.0。

当然我们也可以使用万能命令 go mod tidy 来帮助我们降级,但前提是首先要用 go mod edit 命令,明确告知我们要依赖 v1.7.0 版本,而不是 v1.8.1,这个执行步骤是这样的:

$ go mod edit -require=github.com/sirupsen/logrus@v1.7.0
$ go mod tidy
go: downloading github.com/sirupsen/logrus v1.7.0

降级后,我们再假设 logrus v1.7.1 版本是一个安全补丁升级,修复了一个严重的安全漏洞,而且我们必须使用这个安全补丁版本,这就意味着我们需要将 logrus 依赖从 v1.7.0 升级到 v1.7.1。

我们可以使用与降级同样的步骤来完成升级,这里我

$ go get github.com/sirupsen/logrus@v1.7.1
go: downloading github.com/sirupsen/logrus v1.7.1
go get: upgraded github.com/sirupsen/logrus v1.7.0 => v1.7.1

但是你可能会发现一个问题,在前面的例子中,Go Module 的依赖的主版本号都是 1。根据我们上节课中学习的语义导入版本的规范,在 Go Module 构建模式下,当依赖的主版本号为 0 或 1 的时候,我们在 Go 源码中导入依赖包,不需要在包的导入路径上增加版本号,也就是:

import github.com/user/repo/v0 等价于 import github.com/user/repo
import github.com/user/repo/v1 等价于 import github.com/user/repo

但是,如果我们要依赖的 module 的主版本号大于 1,这又要怎么办呢?接着我们就来看看这个场景下该如何去做。

添加一个主版本号大于 1 的依赖

语义导入版本机制有一个原则:如果新旧版本的包使用相同的导入路径,那么新包与旧包是兼容的。也就是说,如果新旧两个包不兼容,那么我们就应该采用不同的导入路径。

按照语义版本规范,如果我们要为项目引入主版本号大于 1 的依赖,比如 v2.0.0,那么由于这个版本与 v1、v0 开头的包版本都不兼容,我们在导入 v2.0.0 包时,不能再直接使用 github.com/user/repo,而要使用像下面代码中那样不同的包导入路径:

import github.com/user/repo/v2/xxx

也就是说,如果我们要为 Go 项目添加主版本号大于 1 的依赖,我们就需要使用“语义导入版本”机制,在声明它的导入路径的基础上,加上版本号信息。我们以“向 module-mode 项目添加 github.com/go-redis/redis 依赖包的 v7 版本”为例,看看添加步骤。

首先,我们在源码中,以空导入的方式导入 v7 版本的 github.com/go-redis/redis 包:

package main

import (
_ "github.com/go-redis/redis/v7" // “_”为空导入
"github.com/google/uuid"
"github.com/sirupsen/logrus"
) func main() {
logrus.Println("hello, go module mode")
logrus.Println(uuid.NewString())
}

接下来的步骤就与添加兼容依赖一样,我们通过 go get 获取 redis 的 v7 版本:

不过呢,这里说的是为项目添加一个主版本号大于 1 的依赖的步骤。有些时候,出于要使用依赖包最新功能特性等原因,我们可能需要将某个依赖的版本升级为其不兼容版本,也就是主版本号不同的版本,这又该怎么做呢?

我们还以 go-redis/redis 这个依赖为例,将这个依赖从 v7 版本升级到最新的 v8 版本看看。

升级依赖版本到一个不兼容版本

我们前面说了,按照语义导入版本的原则,不同主版本的包的导入路径是不同的。所以,同样地,我们这里也需要先将代码中 redis 包导入路径中的版本号改为 v8:

import (
_ "github.com/go-redis/redis/v8"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)

接下来,我们再通过 go get 来获取 v8 版本的依赖包:

$ go get github.com/go-redis/redis/v8
go: downloading github.com/go-redis/redis/v8 v8.11.1
go: downloading github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
go: downloading github.com/cespare/xxhash/v2 v2.1.1
go get: added github.com/go-redis/redis/v8 v8.11.1

这样,我们就完成了向一个不兼容依赖版本的升级。是不是很简单啊!

但是项目继续演化到一个阶段的时候,我们可能还需要移除对之前某个包的依赖。

移除一个依赖

我们还是看前面 go-redis/redis 示例,如果我们这个时候不需要再依赖 go-redis/redis 了,你会怎么做呢?

你可能会删除掉代码中对 redis 的空导入这一行,之后再利用 go build 命令成功地构建这个项目。

但你会发现,与添加一个依赖时 Go 命令给出友好提示不同,这次 go build 没有给出任何关于项目已经将 go-redis/redis 删除的提示,并且 go.mod 里 require 段中的 go-redis/redis/v8 的依赖依旧存在着。

我们再通过 go list 命令列出当前 module 的所有依赖,你也会发现 go-redis/redis/v8 仍出现在结果中:

$ go list -m all
github.com/bigwhite/module-mode
github.com/cespare/xxhash/v2 v2.1.1
github.com/davecgh/go-spew v1.1.1
... ...
github.com/go-redis/redis/v8 v8.11.1
... ...
gopkg.in/yaml.v2 v2.3.0

这是怎么回事呢?

其实,要想彻底从项目中移除 go.mod 中的依赖项,仅从源码中删除对依赖项的导入语句还不够。这是因为如果源码满足成功构建的条件,go build 命令是不会“多管闲事”地清理 go.mod 中多余的依赖项的。

那正确的做法是怎样的呢?我们还得用 go mod tidy 命令,将这个依赖项彻底从 Go Module 构建上下文中清除掉。go mod tidy 会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除。

到这里,其实我们已经分析了 Go Module 依赖包管理的 5 个常见情况了,但其实还有一种特殊情况,需要我们借用 vendor 机制。

特殊情况:使用 vendor

你可能会感到有点奇怪,为什么 Go Module 的维护,还有要用 vendor 的情况?

其实,vendor 机制虽然诞生于 GOPATH 构建模式主导的年代,但在 Go Module 构建模式下,它依旧被保留了下来,并且成为了 Go Module 构建机制的一个很好的补充。特别是在一些不方便访问外部网络,并且对 Go 应用构建性能敏感的环境,比如在一些内部的持续集成或持续交付环境(CI/CD)中,使用 vendor 机制可以实现与 Go Module 等价的构建。

和 GOPATH 构建模式不同,Go Module 构建模式下,我们再也无需手动维护 vendor 目录下的依赖包了,Go 提供了可以快速建立和更新 vendor 的命令,我们还是以前面的 module-mode 项目为例,通过下面命令为该项目建立 vendor:

$ go mod vendor
$tree -LF 2 vendor
vendor
├── github.com/
│ ├── google/
│ ├── magefile/
│ └── sirupsen/
├── golang.org/
│ └── x/
└── modules.txt

我们看到,go mod vendor 命令在 vendor 目录下,创建了一份这个项目的依赖包的副本,并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。

如果我们要基于 vendor 构建,而不是基于本地缓存的 Go Module 构建,我们需要在 go build 后面加上 -mod=vendor 参数。

在 Go 1.14 及以后版本中,如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非你给 go build 传入 -mod=mod 的参数。

总结

在通过 go mod init 为当前 Go 项目创建一个新的 module 后,随着项目的演进,我们在日常开发过程中,会遇到多种常见的维护 Go Module 的场景。

其中最常见的就是为项目添加一个依赖包,我们可以通过 go get 命令手工获取该依赖包的特定版本,更好的方法是通过 go mod tidy 命令让 Go 命令自动去分析新依赖并决定使用新依赖的哪个版本。

另外,还有几个场景需要你记住:

  • 通过 go get 我们可以升级或降级某依赖的版本,如果升级或降级前后的版本不兼容,这里千万注意别忘了变化包导入路径中的版本号,这是 Go 语义导入版本机制的要求;
  • 通过 go mod tidy,我们可以自动分析 Go 源码的依赖变更,包括依赖的新增、版本变更以及删除,并更新 go.mod 中的依赖信息。
  • 通过 go mod vendor,我们依旧可以支持 vendor 机制,并且可以对 vendor 目录下缓存的依赖包进行自动管理。

Go Module使用 六大场景讲解示例的更多相关文章

  1. python 反射机制在实际的应用场景讲解

    剖析python语言中 "反射" 机制的本质和实际应用场景一. 前言 def s1(): print("s1是这个函数的名字!") s = "s1&q ...

  2. 【Python】python 反射机制在实际的应用场景讲解

    剖析python语言中 "反射" 机制的本质和实际应用场景一. 前言 def s1(): print("s1是这个函数的名字!") s = "s1&q ...

  3. Java BitSet使用场景和示例

    一.什么是BitSet? 注:以下内容来自JDK API: BitSet类实现了一个按需增长的位向量.位Set的每一个组件都有一个boolean值.用非负的整数将BitSet的位编入索引.可以对每个编 ...

  4. ssh登陆linux服务器 实际场景讲解 让你管理服务器更安全

    很多时候我们管理linux系统,都谁使用ssh登陆,因为都知道ssh是加密传输的协议的,可以有效保证我们与 服务器之间的数据通信安全.但是我们忽略了一点,但是登陆的时候我们是输入的账号和密码,这一点其 ...

  5. pythondifflib模块讲解示例

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/Lockey23/article/details/77913855 difflib模块提供的类和方法用 ...

  6. Firefly的角色跳转场景简单示例

    源地址:http://bbs.9miao.com/thread-45790-1-2.html 本例演示的是模拟游戏服务端,让角色在场景1中跳转到场景2中.在实际游戏中,client将要跳转的角色id和 ...

  7. 微信小程序WXML页面常用语法(讲解+示例)

    (一) WXML 是什么 官方说明:WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件.事件系统,可以构建出页面的结构 在前面我们就已经提过,WXML,就可 ...

  8. 前端模块化IIFE,commonjs,AMD,UMD,ES6 Module规范超详细讲解

    目录 为什么前端需要模块化 什么是模块 是什么IIFE 举个栗子 模块化标准 Commonjs 特征 IIFE中的例子用commonjs实现 AMD和RequireJS 如何定义一个模块 如何在入口文 ...

  9. ibeacon的使用和应用场景简单示例

    目的,用ibeacon实现签到功能,不需要太严谨,只是试水. 拿到ibeacon的第一感觉是,这东西能用嘛,2-3年的电池,后面商家说是用个3M双面胶找个地方一贴就行,感觉不太靠谱,嘿嘿,在网上找了一 ...

  10. Spring事务讲解示例

    Spring 事务Transaction1.事务的属性1.1 事务隔离IsolationLevel1.2 事务传播PropagationBehavior1.3 事务超时Timeout1.4 只读状态R ...

随机推荐

  1. P10936 导弹防御塔 题解

    题目链接 题目大意 城堡有 m 个敌人.n 个能发射导弹的防御塔.导弹的速度固定,都为 v.导弹需要 T1 秒发射,T2 分钟冷却,还需要防御塔到敌人距离的 dis/v 的时间.给定防御塔和敌人的坐标 ...

  2. 网络编程入门如此简单(四):一文搞懂localhost和127.0.0.1

    本文由萤火架构分享,原题"localhost和127.0.0.1的区别是什么?",原文链接"juejin.cn/post/7321049446443417638" ...

  3. HTTP协议超级详解【转载】

    HTTP协议简介 超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式.协作式和超媒体信息系统的应用层协议.HTTP是万维网的数据通信的基础. ...

  4. PyScript 使用(1)

    今天按照官方文档进行pyscript的调用,发现paths下总是出现问题,于是调试了一下,问题解决了: # data.py import numpy as np def make_x_and_y(n) ...

  5. nginx basic验证

    打开个生成htpasswd的网站 输入信息生成结果 将结果保存到nginx一个文件里面 修改nginx的conf文件 auth_basic "webA"; #这个"&qu ...

  6. CudaSPONGE与PySAGES初步性能测试

    技术背景 在前面的一篇博客中,我们介绍过CudaSPONGE的基础使用方法.CudaSPONGE调用Python接口函数以及CudaSPONGE结合增强采样软件PySAGES的使用方法.在这篇文章中, ...

  7. 0101-win10 jkd配置注意事项

    更换新的电脑预装win10家庭版,根据常规方法配置jdk8后运行javac提示:不是内部或外部命令,也不是可运行的程序或批处理文件. 1 设置变量classpath时前面有个点(完成这一步后javac ...

  8. 打造有效安全闭环,天翼云MDR来了!

    随着网络攻-防对抗形势愈演愈烈,传统的安全防护模式已难以应对频率暴增.昼夜不停的网络安全攻-击,提升组织安全防护能力势在必行.事实上,一些单位在网络安全建设工作中经验不足,在安全组件/设备采购方面大量 ...

  9. Amazon Dynamo系统架构

    Amazon Dynamo系统架构 目录 Amazon Dynamo系统架构 0x00 摘要 0x01 Amazon Dynamo 1.1 概况 1.2 主要问题及解决方案 1.3 数据均衡分布 1. ...

  10. 考拉 T_Q_X 的博客搬运(搬运)

    博客搬迁现场直播 各位观众们大家好,欢迎来到新闻透视 今天为您直播某菜鸡oier tqx 的博客搬迁现场. Q:请问tqx,您为什么要将博客从CSDN搬迁到博客园呢? tqx:懂得都懂,不懂的我也不多 ...