Go 语言快速上手-工程实践| 青训营笔记

113 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第三篇笔记。

一、语言进阶

1.1 并发与并行

并发表示多线程程序在一个核的cpu上运行

image.png

并行表示多线程程序在多个核的cpu上运行

image.png

Go语言可以充分发挥多核优势,进行高效运行

1.2 进程、线程与协程

  • 进程:程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。

  • 线程:又称轻量级进程,是进程的一个执行实例,是CPU调度和程序执行的最小单位。存在于内核态,内存消耗是MB级别。

  • 协程:在线程中细分出的单位,调度不受操作系统内核所管理,完全由用户控制。存在于用户态,内存消耗是KB级别。

image.png

协程对比线程的优势:

  1. 存在于用户态,可操作性强,调度可由自己控制。
  2. 更轻量,所需资源更少。

1.3 Goroutine

goroutine是 Go 语言并行设计的核心,本质上就是协程。Go 语言内部帮你实现了 goroutine 之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

我们可以通过以下代码实现:

func hello(i int) {
    println("hello world : " + fmt.Sprint(i))
}

func Go() {
    for i := 0; i < 5; i++ {
        go hello(i)
    }
}

代码执行后并没有固定先后顺序。因为它们是多个 goroutine 在执行。

1.4 CSP

Go语言提倡通过通信来共享内存而不是通过共享内存来实现通信。

image.png

1.5 Channel

Channerl是用来传递数据的一个数据结构。可以用于连个Goroutine之间通过传递一个指定类型的值来同步运行和通讯。

Channerl(通道)需要使用make()关键字来进行创建又通过是否有缓冲大小来区分Channerl(通道)类型 并且Channerl(通道)利用操作符 <- 用于指定通道的方向,发送或接收。

ch1 := make(chan int)        //无缓冲通道
ch2 := make(chan int2)     //有缓冲通道

ch := make(chan int)
ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v           

有无缓冲通道的一个区别如下图所示:

image.png

  • 无缓冲通道进行通信时,会导致发送的Goroutine和接收的Goroutine发生同步化。解决同步问题就是使用有缓存通道。

  • 有缓冲的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。但是缓存大小是有限的,如果缓冲区满了,则发送端就无法再次发送数据。这时候如果通道不再接收数据,我们可以通过close()函数进行对Channerl(通道)关闭。

二、依赖管理

在Go语言的开发复杂项目时,工程项目不可能基于标准库0~1编码搭建,随着版本的不断更迭,Go语言依赖管理方面也在不断的完善

以下为Go语言主要的三个依赖管理演进

image.png

2.1 GO PATH

首先GOPATH是Go语言支持的一个环境变量,是Go项目的一个工作区,在工作区中有bin,pkg,src三个文件目录

  • bin -- 负责存放项目编译的二进制文件

  • pkg -- 负责存放项目编译的中间产物,加速编译

  • src -- 负责存放项目源码

在GOPATH中项目代码直接依赖于src下的代码,可以通过go get 下载最新版本的包到src目录下,但是GOPATH的弊端也十分的明显,如果两个项目依赖于某一个package的不同版本,GOPATH就无法实现package的多版本控制

image.png

2.2 GO VECTOR

随着Go语言的迭代,Go语言从 1.5 版本开始开始引入 vendor 模式

这个模式在原有的项目目录下增加了vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor

依赖寻址方式:vendor目录 => GOPATH

通过对每个项目引入一份依赖副本,解决多个项目需要同一个package依赖的冲突问题

当然Go Vendor也会有相应的弊端:

  • 无法控制依赖的版本

  • 更新项目又可能出现依赖冲突,导致编译出错。

归根结底Vendor不能很清晰的表示依赖的版本概念 image.png

2.3 GO MODULE

为了解决Go Vendor遗留的弊端问题,Go Module就应运而生了

Go Module是Go语言官方退出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多个版本等问题。

Go Module从Go 1.11开始实验性引入,Go 1.16默认开启。

Go Module 通过 go.mod文件管理依赖包版本,通过go.get / go mod指令工具管理依赖包

2.4 依赖管理三要素

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

三、测试

在实际工程开发中,另一个重要概念就是单元测试,这一节我们主要讲解Go测试相关的内容,包括单测规范,测试模拟,以及基准测试。测试关系着系统的质量,质量则决定着线上系统的稳定性,一但出现bug漏洞,就会造成事故,因此测试就是避免事故的最后一个屏障。

image.png

3.1 测试类型

测试一般分为

  • 回归测试:用于手动通过终端回归一些固定的主流程场景
  • 集成测试:对系统功能维度做测试验证
  • 单元测试:为开发阶段开发者对单独的函数与模块做功能验证

它们的层级如下,从上到下覆盖率逐层变大,成本却逐层降低

image.png

3.2 单元测试的规则

在Go语言下我们首先需要导入test包才能够实现测试操作,并且遵守有一些单元测试的基本规范:

  • 所有的测试文件以_test.go结尾
  • func TestXxx(t *testing.T)作为被运行的方法
  • 初始化逻辑放到TestMain当中

3.3 单元测试的覆盖率

在单元测试当中,一个代码测试覆盖率,代表了一个代码是否经过了足够的测试,也是评定一个项目的测试水准,更能评估一个项目是否达到了高水准的测试等级

我们这次利用一个判断是否及格的函数来实现代码测试覆盖率的操作 首先我们需要通过go get引入一个我们需要的包 首先我们依旧建立两个文件,一个命名为 Judge.go,一个命名为Judge_test.go的文件。在Judeg.go中我们写入以下函数用来检测分数是否为合格60分。

func Judge(score int16) bool {
	if score >= 60 {
		return true
	}
	return false
}

接下来我们在Judge_test.go的文件中写入以下测试代码:

func TestJudgeTrue(t *testing.T) {
	isPass := Judge(70)
	assert.Equal(t, true, isPass) //通过assert包进行打印
}

最后我们在终端界面输入以下代码并用指定cover参数来查看代码运行的覆盖率

 go test Judge_test.go Judge.go --cover

最后得出了结果为

ok      command-line-arguments  2.913s  coverage: 66.7% of statements

从上述代码得出,我们使用分数为70的值进入Judge代码后,只覆盖到了2/3的代码,这是因为,当我们判断分数后,分数大于等于60,函数就返回了true值,剩下的函数返回false值并没有被覆盖到,所以只执行了2/3。

接下来我们往Judge_test.go文件中再写入一个不及格分数的测试用例后,再次在终端进行执行我们的指令,这次的覆盖率就能够达到100%了

func TestJudgeFalse(t *testing.T) {         //测试返回False函数
	isPass := Judge(70)
	assert.Equal(t, false, isPass)
}
//终端执行命名输出结果
ok      command-line-arguments  3.679s  coverage: 100.0% of statements

Tips

  • 一般覆盖率:50%~60%,较高覆盖率80%+
  • 测试分支相互独立、全面覆盖。
  • 测试单元粒度足够小,函数单一职责。

3.4 单元测试的打桩

打桩就是用桩函数代替原本的函数。

打桩的目的主要有:隔离、补齐、控制。

  • 隔离是指将测试任务从产品项目中分离出来,使之能够独立编译、链接,并独立运行。隔离的基本方法就是打桩,将测试任务之外的,并且与测试任务相关的代码,用桩来代替,从而实现分离测试任务。例如函数A调用了函数B,函数B又调用了函数C和D,如果函数B用桩来代替,函数A就可以完全割断与函数C和D的关系(隔离了A和C、D,而不是隔离A和B)。
  • 补齐是指用桩来代替未实现的代码,例如,函数A调用了函数B,而函数B由其他程序员编写,且未实现,那么,可以用桩来代替函数B,使函数A能够运行并测试。补齐在并行开发中很常用。
  • 控制是指在测试时,人为设定相关代码的行为,使之符合测试需求。

开源Mock测试库-monkey

monkey库:github.com/bouk/monkey

monkey库实现打桩的原理:在运行时通过通过 Go 的 unsafe 包,将内存中函数的地址替换为运行时函数的地址,使测试时调用的函数是打桩函数,实现了Mock的功能。

image.png