工程中的Go语言 | 青训营笔记

68 阅读4分钟

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

课程重点

  1. Go语言并发处理
  2. Go语言依赖管理
  3. Go语言测试流程

Go语言并发处理

  • Gorountine

当计算机从单核时代进化到多核时代时,多线程就是一个离不开的话题

Go语言作为一名新生语言,自然是可以充分发挥多核的优势,高效运行

在Golang中,用于实现多线程并发处理的机制叫做Gorountine

Gorountine可以快速,低占用的创建新协程实现任务并发处理

协程与线程相似但不同于线程,更加轻量化,创建也更加迅速且占用较小

如果我们要创建Gorountine进行并发也很简单,只需要在函数前加一个go即可:

go HelloWorld()  //这行代码会将HelloWorld函数使用Gorountine进行并发运行
  • Channel

沟通是协作的基础,人类是这样,程序也是这样

在Go语言中,Go支持并行的Gorountine之间通过通信共享内存,也支持通过共享内存通信

屏幕截图 2023-01-16 201647.png

但是更加提倡使用通信共享内存

在Go中,我们可以通过创建Channel实现并行任务间的通信

使用a := make (chan 元素类型[缓冲大小])即可创建一个Channel名为a

Channel可以像变量一样使用

func CalSquare() {        //这是使用多线程求从1到9的平方的函数
	src := make(chan int)  //这是两个Channel
	dest := make(chan int, 3)
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {   //产生数数字
			src <- i
		}
	}()
	go func() {
		defer close(dest)           //计算平方
		for i := range src {
			dest <- i * i
		}
	}()
	for i := range dest {              //打印输出
	        println(i) 
	}
}

当一个函数执行较为复杂或者明显较慢的话就要使用缓冲区, 防止“拖累”其他进程

  • Lock

在多人合作时,我们有小概率可能会遇到一种很尴尬的情况 ——

当两个人很巧的在同一时间做了同一件事

放在计算机中就是多个Gorountine同时操作了同一块内存

那么他们总会有一个人的工作是无效的,为了解决这个问题,Go引入了Lock机制:

func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock()     //加锁
		x += 1
		lock.Unlock()   //解锁
	}
}

在这个函数中,我们在对内存进行操作时将它锁起来,操作完成再解锁

就可以很好的解决上面的问题,让函数执行结果无误

  • WaitGroup

有时候我们会把工作完全交给协程去做,但是这个时候主线程就没有任务了

如果主线程继续执行,程序就会完全退出,协程也就会退出从而无法达到目的

WaitGroup就可以很好的解决这个问题:

var wg sync.WaitGroup   //创建名为wg的WaitGroup
wg.Add(5)               //创建五个计数器
wg.Done()               //计数器 - 1

在计数器归零之前,主线程不会结束,而是会持续等待

Go语言依赖管理

众所周知,这个世界上还有许许多多的程序员

而我们作为时代新人,应该学会站在巨人的肩膀上

所谓站在巨人的肩膀上,就是我们可以使用他人写好的代码来辅助我们完成项目

而依赖就是他人已经编写好的,我们可以直接使用的代码包

  • GOPATH

在第一个版本的依赖管理系统中,我们所有的依赖包都存储在GOPATH下的src目录中

这就产生了一个问题,当项目A与项目B需要同一个但是不同版本的依赖包时

屏幕截图 2023-01-16 210335.png

如果我们这时编译则必然有一个项目无法正常编译

于是,为了解决对依赖包多版本的控制问题,Go语言依赖管理迈入了2.0版本

  • Go Vendor

在第二个版本的依赖管理程序中,通过在每个项目中添加vendor文件夹,在每个项目引入一份依赖包副本,成功的解决了依赖包多版本控制问题

但是此时的依赖包管理程序无法控制依赖版本,当依赖更新时,编译依旧会出错

  • Go Module

这是目前最成熟的Go语言依赖管理程序,也是目前正常使用的版本

这个版本通过go.mod文件管理项目依赖

通过go get或者go mod指令管理依赖

屏幕截图 2023-01-16 211904.png

补充说明:

v1.2.3 - 语义版本

格式为:{MAJOR}.{MINOR}.{PATCH} 大版本(Major)不一致可能会不兼容

v1.2.3-20221010111230-a12s5d4f - 基于commit的版本

格式为:语义版本-提交日期-commit哈希

屏幕截图 2023-01-16 212115.png

  • go.mod文件 image.png

当Major版本大于2但是文件夹中没有版本标识(如pkg/v3) 则会注明+incompatible

  • Go语言依赖结构 - 依赖分发

在开源代码托管平台上的依赖包很容易被作者删除或者其他原因导致我们无法使用

于是Go语言引入了Proxy作为稳定可靠的依赖包管理平台

在GOPROXY中保存了管理平台服务器站点的URL列表,如:

GOPROXY="https://proxy1.com,https://proxy2.com,direct"

其中direct代表源站,Go会横向依次访问服务器获取依赖

Go语言测试流程

  • 单元测试

测试是程序开发中不可或缺的流程,代码测试保证代码质量,提升代码效率

而单元测试正是其中最基础的一种,它的流程是这样的:

image.png

其实用单元测试说白了也就是对每个实现功能的函数进行测试,并与给出的期望进行对比

编写单元测试程序时需要遵守以下几个规则:

  • 测试程序文件名以_test结尾,如func_test.go
  • 测试函数以Test函数名命名,参数为*Testing.T,如TestXxx(t *Testing.t)
  • 初始化逻辑放在TestMain函数中

在测试中也可以用到testift中的assert来帮助测试:

func TestHelloTom(t *testing.T) {
	output := HelloTom()   //原函数会输出一个名字
	expectOutput := "Tom"
	assert.Equal(t, expectOutput, output)  //测试输出是否是Tom
}

编写好程序,输入go test xxx_test.go xxx.go就可以开始测试了

在测试过程中,也可以使用--cover后缀来查看测试代码覆盖率

一般的程序测试覆盖率在50% ~ 60%,重点程序(如支付类)则需要覆盖率在80%+

  • Mock

当我们进行的测试需要改变外界条件时,我们就需要不断的为其创造条件,这是繁琐的

这时我们就可以利用Monkey依赖进行Mock测试

func TestProcessFirstLineWithMock(t *testing.T) { //原函数会将文件中的11改为00
	monkey.Patch(ReadFirstLine, func() string { 
		return "line110"   //通过Mock将读取文件的操作改为直接返回line110
	})
	defer monkey.Unpatch(ReadFirstLine)
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)  //测试修改结果是否符合预期
}

这样我们便不需要进行繁琐的准备操作

  • 基准测试

在许多环境中,我们需要知道代码的运行速度,并且让代码更迅速的运行

这时,基准测试便是我们最好的选择

基准测试的函数通常以Benchmark开头,参数为*Testing.B,示例如下:

func BenchmarkXxx(b *testing.B) {}

在基准测试中,Go会为我们返回运行函数所需的时间

这将作为我们优化函数的参考,为我们提供很大的帮助

image.png

那么今天的笔记就到这里了,我们明天再见!