Go语言依赖管理 | 青训营笔记

719 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

简介

不重复造轮子,很多人都听过这话。

我们平时开发程序,不可能所有代码都从零到一实现,不可避免的要引用别人的代码。

但这就引入了一个问题,代码间的依赖关系以及不同版本依赖如何进行管理?

这就引入了依赖管理工具。

常见Go环境变量

使用go env 命令可以查看环境变量。

常见的环境变量有

set GO111MODULE="on" # 默认开启,代表开启Go Module 

set GOROOT="/Users/xxx/.g/go" # go安装目录 
set GOPATH="/Users/xxx/Repository/gopath" # 本地仓库地址
set GOMODCACHE=$GOPATH/pkg/mod    # Go Module 地址

set GOPROXY="https://goproxy.cn,direct" # 代理仓库配置
set GOPRIVATE="git.xxx.cn" # 私有仓库配置,没有则不配

Go依赖管理演进历程

Go Path

简介

项目的工作目录。

用来存放开发过程中产生的源代码、中间文件、二进制文件。

目录结构

  • bin 编译后的二进制文件目录
  • pkg 中间代码
  • src 源代码目录

设计思路

所有项目的依赖文件都存放在 src 目录下。

所有的项目需要依赖时直接从src 目录下查找,找不到再通过go get/install命令安装。

使用 go getgo install 命令下载的源码会存放在 src 目录下

缺点:无法实现多版本控制

GOPATH缺陷.jpg 当 Project A 与 Project B 所依赖包的版本不同,且版本间不兼容时,

引入ProjectB会下载新的依赖版本,覆盖掉原有版本(也就是 GOPATH 管理依赖方式,一个包只能保存一个版本)。

如果 Project A 恰好依赖了新版本已经不兼容的特性,这会使得 依赖原有包的 Project A。在引入新的依赖版本后,出现问题。

为了解决这个问题,Go 1.5 版本引入了Vendor机制。

Go Vendor

简介

为了解决 GOPATH 依赖管理 不能实现多依赖控制的问题,在Go 1.5 版本引入的机制。

设计思路

让每个项目都拥有自己的依赖存放目录,而不是共享 GOPATH 目录下依赖。

在项目根路径下创建 vendor 文件夹,并规定依赖读取的顺序是

$ProjectRoot/vender -> $GOPATH/src

也就是,优先读取项目中 vendor 目录下的依赖,读取不到,才会读取 GOPATH 目录下的依赖。

改进

GOPATH缺陷.jpg

引入 Vendor机制后,每个项目都拥有自己的 vendor 目录,从自己的目录可以读取专属于自己的依赖版本,这自然就解决了GOPATH依赖管理方式,多个项目只能依赖同一依赖版本的问题。

问题

  • 没有实现多版本管理,项目的 vendor 目录保存的依旧是文件,而不是具体版本。
  • 更新项目时,有可能出现依赖冲突,导致编译出错。

Go Module

简介

GO 1.11版本引入的管理机制,从此Go语言依赖管理机制基本成熟。

设计思路

成熟依赖管理工具必备三要素

  • 配置文件、描述依赖关系

类似Maven中的pom.xml文件,文件中定义了项目依赖

  • 中心仓库存储、分发依赖

类似Maven中央仓库,存放已提交的所有依赖,并承担依赖分发工作。

  • 本地工具

管理工具,类似Maven命令,用来控制依赖管理程序。

大体的思路是,项目构建时,根据配置文件中声明的依赖版本,去中心仓库下载包到本地,进而编译。

这样做的优势是,项目不直接依赖源代码,而是依赖配置文件,通过配置文件间接的依赖源代码,实现了二者的解耦,方便依赖管理。

故 Go 想要实现成熟的依赖管理,也要实现上述内容。

配置文件实现 --- go.mod

简介

$ProjectRoot目录下,记录了当前项目依赖信息的文件。

特点

  • go.mod中记录的包版本,必须保证在代码托管平台中只有一次提交与之对应。
    • 不这样的话,go.mod中声明一个版本,从代码托管平台获取源码时,却获取到了多个不同版本的源代码,会出现二义性。
  • Go 语言规定 vN (N>=2) 及以上版本的依赖包,模块名后都要加上/vN后缀,拥有全新的导入路径。

依赖声明格式

为了避免上述二义性,就需要依赖声明能够唯一标识依赖包。

Go 采用 模块名 + 版本号 唯一标识。

模块名

模块名在go.mod文件中,由module 指令指定。

module example.com/my/thing,模块名就是example.com/my/thing

版本号

版本号有两种表示方式。

  • 基于tag
    • Major(可不兼容更新) + Minor(功能性更新) + patch(bugfixs)
  • 基于commit hash的伪版本
    • vx.0.0-yyyymmddhhmmss(当前时间)-abcdefgh1234(12位hash)

示例

// 唯一标识示例
example.com/other/thing v1.0.2 

go.mod示例(附常用指令介绍)

module example.com/my/thing       // module 指令指定模块名

go 1.12                           // go 指令指定 go 版本号

require example.com/other/thing v1.0.2 
// require 指令指定依赖包的最小可用版本(见最小版本抉择)。

require example.com/new/thing/v2 v2.3.4 
// Go 语言规定 vN (N>=2) 及以上版本的依赖包,模块名后都要加上/vN后缀

require example.com/big/thing v3.2.0+incompatible
// 对于开发比较早的依赖包可能在Go module推出前已经打上 vN (N>=2) 的tag,
// 为了兼容这部分依赖包,对于没有 go.mod 文件 且 vN (N>=2) 的依赖包,在版本号后加上 +incompatible 来标识

require example.com/samll/thing v1.0.2 // indirect
// indirect后缀,表明当前依赖包不是直接依赖,而是间接依赖

exclude example.com/old/thing v1.2.3   
// exclude 指令排除某个依赖的版本。

replace example.com/bad/thing v1.4.5 => example.com/good/thing v1.4.5
// replace 指令可以用指定内容替换一个模块的特定版本,或一个模块的所有版本。
// 替换可以指定另一个模块的路径和版本,或者一个特定平台的文件路径。

retract [v1.9.0, v1.9.5]
// 指定当前项目的某个版本或一系列版本不应该被依赖。
// 往往用于版本发布过早,或者出现重大错误的情况,来撤回某个版本。
// 并且在云端保留该版本,保证已经依赖该版本的程序能够正常依赖。

中心仓库实现 ---- Proxy

简介

image.png

项目中依赖包都可以在代码托管平台中找到某个项目的提交与之对应。

但代码仓库拥有者可以对自己发布的源码进行添加、修改、删除操作,这就导致了直接下载代码仓库源码并构建程序的方案,具有不稳定性。

为了解决这个问题,Go引入一个中间层Proxy,对代码仓库内容进行缓存,制定了一系列规则,保证了基于Proxy构建软件的稳定性。

Proxy规则

  • Proxy是一个HTTP服务器,可以响应对指定目录的Get请求
  • 项目构建时,在缓存中找不到依赖的包,会对GOPROXY解析,发起Get请求。
  • 成功的请求返回 200 响应码、源码。
  • 未找到的请求返回404 或者 410 响应码(继续找下一个Proxy)
  • 失败的请求返回 4xx 或 5xx 响应码(结束)
  • Proxy必须响应以下请求
    • base/base/module/@v/list   获取模块版本列表
    • $base/wmodule/@v/wrversion.info   返回特定版本元数据(json格式)
    • $base/wmodule/@v/wrversion.mod   返回特定版本go.mod文件
    • $base/wmodule/@v/wrversion.zip   返回特定版本 zip 文件 (源码)
    • base/base/module/@latest   返回最新版本元数据
  • Proxy缓存的源码存放在 $GOMODCACHE/cache目录下

配置Proxy服务器

直接修改GOPROXY变量可以配置Proxy服务器。

可以使用 , 或者 | 来作为分隔符,其中 , 分隔符代码查找不到(返回 404,410 响应码)才继续查找,| 表示无论返回什么,不成功就继续查找。

direct 关键字代表直接访问代码托管平台(如Github)

示例

GOPROXY="https://goproxy.cn,direct" # 代理仓库配置

本地工具实现 ---- go get/go mod

go get

image.png

用法

// 为当前项目拉取依赖
go get example.org/pkg
// 下载的依赖包放到缓存中,并在go.mod文件中新增一条require记录

// 将当前项目依赖升/降级到指定版本
go get example.org/pkg@v1.3.4

go mod

image.png

用法

// 为当前项目拉取依赖
go tidy
// 分析源码,自动拉取依赖

// 将当前项目依赖升/降级到指定版本
go mod edit -require=example.org/pkg@v1.7.0

原理解析

go get/mod 会将 命令行参数/go.mod 中 require 命令声明的唯一标识 解析为url,发出请求,获取到依赖包,保存到 $GOPATH/pkg/mod目录下。

项目获取依赖包时,会先向 Proxy 根据唯一标识请求依赖包,请求不到才会访问源存储库。

如果模块名末尾中包含VCS限定符(.bzr.fossil.git.hg.svn 之一),使用该路径限定符之前的所有内容作为存储库 URL。

比如 example.com/foo.git/bar

Go 语言会使用 git 访问 example.com/foo.git, 并尝试在 bar 子目录下查找模块代码。

对于模块名末尾不包含 VCS 限定符的,会像派生的 url 发送 get 请求,并携带查询字符串 go-get=1

比如模块 golang.org/x/mod

可能发送 https://golang.org/x/mod?go-get=1请求

获取到响应后,根据响应htmnl的meta标签定位到代码托管仓库,获取源代码。

最小版本抉择

简介

最小版本抉择机制是Go语言提出的高保真构建算法。

能够用户构建的软件与作者构建的软件尽可能的采用相同的依赖版本,并且实现简单,只需要几百行代码。

算法流程图

image.png

最后需要补充该操作。 image.png

注:构造构建列表有两种方式,一种递归式(上述),一种图遍历方式。 实际使用的算法是图遍历方式,因为递归式算法性能太差,以及会出现循环依赖问题。之所以在这里列出来,是为了方便理解构造构建列表的过程。

示例

image.png

对于上述依赖图,构造构建列表时,会先将 Main 加入构建列表。然后遍历 Main 的go.mod文件,先遍历 A 1.2,将A 1.2 依赖包加入构建列表,然后进行深度遍历,将之后的 C 1.3、D 1.2 依赖包加入构建列表。 在深度遍历 B 1.2, 将 B 1.2、C 1.4、D 1.2加入构造列表。这就得到了粗略的构造列表.

[A 1.2,B 1.2,C 1.3,C 1.4,D 1.2]

最后移除 C 模块的旧版本 1.3,只保留最新版本 1.4。

得到最终的构建列表。

[A 1.2,B 1.2,C 1.4,D 1.2]

四、课后个人总结:

对于依赖管理模块只是简单讲解,课后做了简单补充。

五、引用参考: