day4 依赖管理和测试 | 青训营笔记

237 阅读5分钟

题记

这是我参与「第五届青训营 」伴学笔记创作活动的第 4天,本文用于记录在青训营的学习笔记和一些心得。

day4 1月18日

依赖管理

学会站在巨人的肩膀上(要学会抄,不是)

go依赖管理演进

image-20230116161925565

目前被广泛应用的是 Go Module,整个演进路线主要围绕实现两个目标来迭代发展:

  • 不同环境 (项目) 依赖的版本不同;
  • 控制依赖库的版本。
GoPATH

GOPATH 是 Go 语言支持的一个环境变量,是 Go 项目的工作区。其目录有以下 3 个结构 (需要手动创建文件夹):

image-20230116162122723

弊端

下面的场景就体现了 GOPATH 的弊端:项目A 和项B 依赖于某一 package 的不同版本 (分别为 Pkg V1Pkg V2 ) 。而 src 下只能允许一个版本存在,那项目A 和项B 就无法保证都能编译通过。

image-20220517141844767

在 GOPATH 管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,无法做到不同项目依赖同一个库的不同版本。这显然无法满足实际开发中的项目依赖需求,为了解决这个问题,Go Vendor 出现了。

Go Vendor
  • 与 GOPATH 不同之处在于项目目录下增加了 vendor 文件,所有依赖包以副本形式放在 $ProjectRoot/vendor 下。

  • 在 Vendor 机制下,如果当前项目存在 Vendor 目录,会优先使用该目录下的依赖;如果依赖不存在,则会从 GOPATH 中寻找。这样,通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题。

    image-20230116162351099

弊端

但 Vendor 无法很好解决依赖包版本变动问题和一个项目依赖同一个包的不同版本的问题。

image-20230116162814685

Go Module
  • Go Module 是 Go 语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多个版本等问题。
  • Go Module 自 Go1.11 开始引入,Go 1.16 默认开启。可以在项目目录下看到 go.mod 文件:
名称作用
go.mod文件,管理依赖包版本
go get / go mod指令,管理依赖包

【终极目标】定义版本规则和管理项目依赖关系。和 Java 中的 Maven 作用是一样的。

依赖管理三要素

要素对于工具
配置文件,描述依赖go.mod
中心仓库管理依赖库Proxy
本地工具go get / go mod
依赖配置—go.mod

image-20230116163028625

【module 路径 (上图的“依赖管理基本单元”)】用来标识一个 module,从 module 路径可以看出从哪里找到该 module 。例如,如果以 github 为前缀开头,表示可以从 Github 仓库找到该 module 。依赖包的源代码由 Github 托管,如果项目的子包想被单独引用,则需要通过单独的 init go.mod 文件进行管理。

【原生库】依赖的原生 Go SDK 版本。

【单元依赖】每个依赖单元用 module路径 + 版本号 来唯一标识。

依赖配置-version
  • GOPATH 和 Go Vendor 都是源码副本方式依赖,没有版本规则概念。
  • 而 go.mod 为了方便管理,定义了版本规则。分为语义化版本和基于 commit 伪版本两个版本。

1.语义化版本

${MAJOR}.${MINOR}.${PATCH}

如:V1.18.1、V1.8.0

名称含义
MAJOR不同的MAJOR版本表示是不兼容的API。因此即使是同一个库,MAJOR版本不同也会被认为是不同的模块
MINOR通常是新增函数或功能,向后兼容
PATCH一般是修复bug

2.基于 commit 伪版本

每次提交 commit 后,Go 都会默认生成一个伪版本号:

v0.0.0-yyyymmddhhmmss-abcdefgh1234

如:v1.0.0-20220517152630-c38fb59326b7

名称含义
v0.0.0版本前缀和语义化版本是一样的
yyyymmddhhmmss时间戳,提交Commit的时间
abcdefgh1234校验码,包含12位的哈希前缀
依赖配置-indirect

2.3.2 节的 go.mod 文件图中,细心观察可以发现有些单元依赖带有 // indirect 的后缀,这是一个特殊标识符,表示 go.mod 对应的当前 module 没有直接导入的包,也就是非直接依赖 (即间接依赖) 。

image-20230116163543114

例如,一个依赖关系链为:A->B->C 。其中,A->B 是直接依赖;而 A->C 是间接依赖。

依赖配置-incompatible

image-20230116163559667

2.3.2 节的 go.mod 文件图中,细心观察可以发现有些单元依赖带有 +incompatible 的后缀,这也是一个特殊标识符。对于 MAJOR 主版本在 V2 及以上的模块,go.mod 会在模块路径增加 /vN 后缀 (如下图中 example/lib5/v3 v3.0.2 )。这能让 Go Module 按照不同的模块来处理同一个项目不同 MAJOR 主版本的依赖。

  • 由于 Go Module 是在 Go 1.11 才实验性地引入,所以在这个更新提出之前,已经有一些仓库打上了 V2 或者更高版本的 tag 了。
  • 为了兼容这部分仓库,对于没有 go.mod 文件并且 MAJOR 主版本在 V2 及以上的依赖,会在版本号后加上 +incompatible 后缀。表示可能会存在不兼容的源代码。
依赖配置-依赖图

如下图所示,Main 项目依赖项目A 和项目B ,且项目A 和项目B 分别依赖项目C 的 v1.3 和 v1.4 版本。最终编译时,Go 所使用的项目C 的版本为:v1.4 。

【总结】Go 选择最低的兼容版本。

image-20220517160311299

依赖分发-回源
  • 依赖分发,即依赖从何处下载、如何下载的问题。
  • Go 的依赖绝大部分托管在 GitHub 上。Go Module 系统中定义的依赖,最终都可以对应到 GitHub 中某一项目的特定提交 (commit) 或版本。
  • 对于 go.mod 中定义的依赖,则直接可以从对应仓库中下载指定依赖,从而完成依赖分发。

image-20220517161158838

弊端

直接使用 GitHub 仓库下载依赖存在一些问题:

  • 首先,无法保证构建稳定性。代码作者可以直接在 GitHub 上增加/修改/删除软件版本。
  • 无法保证依赖可用性。代码作者可以直接在 GitHub 上删除代码仓库,导致依赖不可用。
  • 第三,如果所有人都直接从 GitHub 上获取依赖,会导致 GitHub 平台负载压力。

解决方案-Proxy

  • Go Proxy 就是解决上述问题的方案。Go Proxy 是一个服务站点,它会缓存 GitHub 中的代码内容,缓存的代码版本不会改变,并且在 GitHub 作者删除了代码之后也依然可用,从而实现了 “immutability” (不变性) 和 “available” (可用的) 的依赖分发。
  • 使用 Go Proxy 后,构建时会直接从 Go Proxy 站点拉取依赖。如下图所示。

image-20220517183641065

依赖分发-变量GOPROXY
  • Go Module 通过 GOPROXY 环境变量控制如何使用 Go Proxy 。

  • GOPROXY 是一个 Go Proxy 站点 URL 列表。

    GOPROXY = "https://proxy1.cn, https://proxy2.cn, direct"
    
  • 上述代码中,direct 表示源站 (如 GitHub) ,proxy1 proxy2 是两个URL 站点。依赖寻址路径为:优先从 proxy1 下载依赖,如果 proxy1 不存在,再从 proxy2 寻找,如果 proxy2 不存在,则会回源到源站直接下载依赖,并缓存到 Go Proxy 站点中 (这种设计思路和 Redis 缓存与 MySQL 数据库一模一样)。

image-20230116164422498

工具—go get

image-20230116164512830

工具—go mod

image-20230116164526547

测试

Learn go with tests要是真的想好好学测试建议看这个。

还是浅浅的记录一点内容吧。

image-20230116180900935

image-20230116180915123

image-20230116181651395

image-20230116181739856

image-20230116182210935

上面图片出现的答案是不是很奇怪,下面我们看看煎鱼大佬是怎么说的

Go 并发:一些有趣的现象和要避开的 “坑”

go test -bench="." //ppt上不是错的,是Linux,windows下用这个
​
​
TestMain()   //进行测试之前需要初始化操作(例如打开连接),测试结束后,需要做清理工作(例如关闭连接)等等。这个时候就可以使用TestMain()。
​
​
//比较值是否相等,不过我更爱我的reflect.DeepEqual()
assert.equal(t,value1,value2) 
​
go test --cover //测试覆盖率
​
​
utf8.RuneCountInString(...)
/*Code:
str := "Hello, 世界" 一个汉字三个字节
fmt.Println("bytes =", len(str))
fmt.Println("runes =", utf8.RuneCountInString(str)) 字符
​
Output:
bytes = 13
runes = 9
*/