这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
一、语言进阶-并发编程
1.1 Goroutine
线程:用户态,轻量级线程,栈MB级别。
协程:内核态,线程跑多个协程,栈KB级别。
1.2 CSP (Communicating Sequential Processes)
左图中的通道连接了不同的携程,类似于一个队列,进行先进先出,使得一个协程能够向另一个协程发送信息。
右图中临界区需要锁之类的,导致性能堪忧。
1.3 Channel
创建一个通道:make(chan元素类型,[缓冲大小])
无缓冲通道/同步通道 make(chan int)
有缓冲通道(生产消费模型) make(chan int,2)
下面展示了Channel工作的例子,其中A和B协程使用src通信,主协程和B之间使用dest通信,由于打印操作耗时较长可能导致阻塞,因此使用有缓冲队列
1.4 并发安全 Lock (通过共享内存实现通信)
1.5 WaitGroup
sleep 由于不知道子协程所需的时间,因此进行暴力阻塞
waitgroup使得所有的协程
如下为主协程等待5个子协程结束后再退出的例子
二、依赖管理
对于hello word以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能甚于标准库0~1编码搭建,而更多的关注业务逻辑的实现,而其他的涉及框架、日志、driver、 以及Collection等- 系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要
2.1 Go依赖管理演进
到目前被广泛应用的go module,整个演进路线主要围绕实现两个目标来迭代发展的,分别是不同环境(项目)依赖的版本不同和控制依赖库的版本。
2.1.1 GOPATH
GOPATH是Go语言支持的一个环境变量,value是Go项目的工作区。存在无法实现pck的多版本控制的问题
如下图,同一个pkg, 有2个版本,A->A(),B->B(),而src下只能有一 个版本存在,那AB项目无法保证都能编译通过。也就是在gopath管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。为了解决这问题,govender出现了。
2.1.2 GOVENDOR
vendor文件为当前项目依赖的副本,即每一个项目都可以存在一个vendor。 在Vendor机制下, 如果当前项目存在Vendor目录,会优先使用该目录下的依赖,如果依赖不存在,会从GOPATH中寻找。
由于vendor是针对一个具体的项目,因此当一个项目中存在packA和pckB需要依赖不同版本的packD时,便存在一些问题。
2.1.3 GoModule
Go Modules是Go语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多个版本等问题,go module从Go 1.11开始实验性引入,Go 1.16 默认开启;我们一般都读为go mod。
通过go.mod文件管理依赖包版本,通过go get/go mod指令工具管理依赖包,终极目标:定义版本规则和管理项目依赖关系
2.2依赖管理三要素
2.3 Gomodule实践
2.3.1 依赖配置go.mod
首先模块路径用来标识一个模块, 从模块路径可以看出从哪里找到该模块,如果是github前缀则表示可以从Github仓库找到该模块,依赖包的源代码由github托管,如果项目的子包想被单独引用,则需要通过单独的init go。mod文件进行管理。
下面是依赖的原生sdk版本。
最下面是单元依赖,每个依赖单元用模块路径+版本来唯-标示。
2.3.2依赖配置-version
gopath和govendor都是源码副本方式依赖,没有版本规则概念,而gomod为了放方便管理则定义了版本规则,分为语义化版本。不同的MAJOR版本表示是不兼容的API,所以即使是同一个库,MAJOR版本不同也会被认为是不同的模块; MINOR版本通常是新增函数或功能,向后兼容;而patch 版本一般是修复bug ;
而基于commit的为版本包括版本前缀、时间戳和哈希码,基础版本前缀是和语义化版本-样的;时间戳wyddhmss,也就是提交Commit的时间,最后是校验码(abcdefabcdef,包含12位的哈希前缀;每次提交commit后 Go都会默认生成一个伪版本号。
2.3.3单元依赖关键字
1.indirct 下图所示的关系中,A对C是间接依赖。
2.incompatible(不兼容)
主版本2 +模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。由于gomodule是实验性引入所以已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible后缀。例如,上图lib6即为gomod打赏了3的tag。
注意c1.3和c1.4是兼容的,因此1.3的功能1.4都能够实现。
2.4依赖分发
gomodule的依赖分发,也就是从哪里下载,如何下载的问题。
github是比较常见给的代码托管系统平台,而Go Modules系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。
但直接使用版本管理仓库下载依赖,存在多个问题,首先无法保证构建确定性:软件作者可以直接代码平台增加/修改/删除软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用:大幅增加第三方代码托管平台压力。
而go proxy就是解决这些问题的方案,Go Proxy是一个服务站点,它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供immutability "和' available "的依赖分发。使用Go Proxy之后,构建时会直接从Go Proxy站点拉取依赖。
关于go proxy的使用,Go Modules通过GOPROXY环境变量控制如何使用Go Proxy; GOPROXY是一个 Go Proxy站点URL列表,可以使用"direct"表示源站。 对于示例配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,后下钻proxy2寻找,如果proxy2, 中不存在则会回源到源站直接下载依赖,缓存到proxy站点中。
三、测试
测试是避免事故的最后屏障。 测试一般分为,回归测试一般是QA同学手 动通过终端回归-些固定的主流程场景 ,集成测试是对系统功能维度做测试验证,而单元测试测试开发阶段,开发者对单独的函数、模块做功能验证,层级从上至下,测试成本逐渐减低,而测试覆盖率确逐步上升,所以单元测试的覆盖率-定程度上决定这代码的质量。
3.1单元测试
3.1.1基础概念
包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符;单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个个较短周期内定位和修复问题。
测试文件和测试函数之间的关系是什么,测试函数只能有一个吗,如何获得预期的输出呢?
以下展示了单元测试的一个简单应用场景,hellotom预期输出Tom,因此测试函数将函数的输出与Tom进行比较,出错时便触发异常。
使用go test[flags][packages]来进行测试,或者直在IDE中点击运行,得到如下所示的结果。
go test命令(Go语言测试命令)完全攻略 (biancheng.net)
需要注意的是,在测试过程中出现如下所示的情况,这是因为没有指定待测试函数的位置,进行修改后,成功解决。
PS E:\C盘镜像\下载\go-project-example-0\test> go test print_test.go
# command-line-arguments [command-line-arguments.test]
.\print_test.go:9:12: undefined: HelloTom
# command-line-arguments [command-line-arguments.test]
.\print_test.go:9:12: undefined: HelloTom
FAIL command-line-arguments [build failed]
FAIL
PS E:\C盘镜像\下载\go-project-example-0\test> go test -v print.go print_test.go
=== RUN TestHelloTom
print_test.go:11:
Error Trace: print_test.go:11
Error: Not equal:
expected: "Tom"
actual : "HXW"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Tom
+HXW
Test: TestHelloTom
--- FAIL: TestHelloTom (0.00s)
FAIL
FAIL command-line-arguments 0.224s
FAIL
此外,在Test函数中,一些输出与预期的比较可以通过assert包进行实现。
3.1.2评判标准
代码覆盖率,代表测试文件中所有测试函数所经过的代码的行数占总行数的比率。
3.1.3 Mock
在下图所示的例子中,测试源码对于测试文件具有较强的依赖。
下图将ReadFirstLine函数转化为返回"line110"的函数,从而解除了对于本地文件的强依赖。
3.1.4 基准测试
Go语言还提供了基准测试框架,基准测试是指测试-段程序的运行性能及耗费 CPU的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试,
我们用下面的命令运行基准测试。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数。
基准测试 · Go语言圣经 (studygolang.com)
项目实战
首先下载gin框架