这是我参与「第五届青训营 」伴学笔记创作活动的第3天,本应该是昨天发这篇笔记的,累了懒了不想动,这里批评自己。本次课程主要学习了go语言工程相关的知识。
语言进阶
并发与并行:
并发和并行最开始都是操作系统中的概念,表示的是CPU执行多个任务的方式。这两个概念极容易混淆。
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。
并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。 这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。 并行的多个任务之间是不互相抢占资源的,
只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
Goroutine
go语言使用go func()来开启一个协程,处理相应的事务,相较于其它语言十分方便。
func hello(){
println("hello")
}
func helloGoRoutine(){
for i:=0;i<5;i++{
go hello()
}
}
Channel
协程之间通信的渠道通过make创建分为有无缓冲区通道两种:
代码实现如下:第一个协程将i传递给src,第二个协程遍历src中的数据平方后传递给dest,最后输出。
并发安全
使用sync包下的sync.Mutex来保证进程通信时的数据安全问题,加锁解锁等。
WaitGroup
比如我们有一个主任务在执行,执行到某一点时需要并行执行三个子任务,并且需要等到三个子任务都执行完后,再继续执行主任务。那我们就需要设置一个检查点,使主任务一直阻塞在这,等三个子任务执行完后再放行。
func main() {
var waitGroup sync.WaitGroup
start := time.Now()
waitGroup.Add(5)
for i := 0; i < 5; i++ {
go func() {
defer waitGroup.Done()
time.Sleep(time.Second)
fmt.Println("done")
}()
}
waitGroup.Wait()
fmt.Println(time.Now().Sub(start).Seconds())
}
/*
done
done
done
done
done
1.000306089
*/
示例中使用waitGroup.Add(5)表示我们有5个子任务,然后起了5个协程去完成任务,主协程使用waitGroup.Wait()方法等待子协程执行完毕,输出一共等待的时间。
WaitGroup 一共有三个方法:
- WaitGroup.Add(int)
- WaitGroup).Done()
- WaitGroup).Wait()
Add 方法用于设置 WaitGroup 的计数值,可以理解为子任务的数量 Done 方法用于将 WaitGroup 的计数值减一,可以理解为完成一个子任务 Wait 方法用于阻塞调用者,直到 WaitGroup 的计数值为0,即所有子任务都完成
依赖管理
GOPATH(初版)
- 项目代码直接依赖src目录下的代码
- go get 下载包到src目录下
缺点:无法实现package的多版本控制
GO Vendor(发展)
- 项目目录下增加vendor目录,存放所有依赖副本
- 依赖的寻址方式: 先vendor再GOPATH
缺点:无法控制依赖版本,更新项目又可能出现依赖冲突
GO Module(目前)
- 通过go.mod文件管理依赖包的版本
- 通过go get/go mod指令工具管理依赖包
依赖管理的三要素:
- 配置文件,描述依赖-----------------go.mod
- 中心仓库管理依赖库 ----------------Proxy
- 本地工具 ---------------------------- go get/mod
go mod与go get
测试相关
概念
通常,有多种测试方法可以使用,例如回归测试,集成测试,单元测试,而单元测试(Unit Test) 是成本最低,覆盖率最高的测试方法。所谓单元测试,便是为代码的每一个模块,函数定制单独的测试,保证输入指定值后输出正常值。通过多个单元测试合并运作,我们便可得知项目的每一个细节都在正确运行,最终得知项目整体运作正常。
进行单元测试
Go 内置单元测试支持。所有以 _test.go 结尾的代码会被 Go 识别为单元测试文件。
一个单元测试函数的函数名应当以 Test 开头,并包含 *testing.T 形参。
可通过 func TestMain(m *testing.M) 函数对测试数据进行初始化,并调用 m.Run() 运行单元测试。
以下代码是一个简单的单元测试例子,测试 HelloTom 函数是否正常返回值 Tom:
// In xxx.go:
func HelloTom() string {
return "Jerry"
}
// In xxx_test.go:
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
并使用 go test 指令运行单元测试,得到运行失败的输出:
=== RUN TestHelloTom
xxx_test.go:9: Expected Tom do not match actual Jerry
--- FAIL: TestHelloTom (0.00s)
复制代码
当然,我们也可以引入社区提供的依赖库来加快单元测试开发,诸如通过 testify/assert 库进行覆盖率测试,或通过 bouk/monkey 库对数据进行 Mock。此处不再赘述。
基准测试
有时,我们想要测量一个函数执行不同次数,或是在不同环境下执行函数的性能,这时,就需要进行基准测试。Go 内置基准测试支持。
以下代码模拟了一个负载均衡的例子,在 10 个服务器中随机返回数据,我们将对 Select 方法分别进行串行和并行的基准测试:
// In xxx.go:
import "math/rand"
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
// In xxx_test.go:
package main
import "testing"
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer() // 重置计数器是因为 InitServerIndex 不应包含在测试时间内
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
并使用 go test -bench= 指令运行基准测试,得到结果:
GOROOT=F:\Program Files\Go #gosetup
GOPATH=C:\Users\shaok\go #gosetup
"F:\Program Files\Go\bin\go.exe" test -c -o C:\Users\shaok\AppData\Local\Temp\GoLand___gobench_helloWorld.test.exe helloWorld #gosetup
C:\Users\shaok\AppData\Local\Temp\GoLand___gobench_helloWorld.test.exe -test.v -test.paniconexit0 -test.bench . -test.run ^$ #gosetup
goos: windows
goarch: amd64
pkg: helloWorld
cpu: AMD Ryzen 7 5800H with Radeon Graphics
BenchmarkSelect
BenchmarkSelect-16 164467417 7.332 ns/op
BenchmarkSelectParallel
BenchmarkSelectParallel-16 26950542 43.85 ns/op
PASS
复制代码
根据基准测试结果,我们可以进行针对性的优化。
项目实战
只是粗略过了一遍 ,没来得及细看,日后补上。