这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
本节重要内容:
Go语言的并发特性、依赖管理、单元测试
Go语言的并发优势
Go语言的并发机制运用起来非常简便,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量。
接下来看几个概念:
并发/并行
多线程程序在单核心的 cpu 上运行,称为并发;
多线程程序在多核心的 cpu 上运行,称为并行。
并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。
协程/线程
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。使用Go语言开发服务器程序时,就需要对它的并发机制有深入的了解。
Goroutine 介绍
goroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务,它是Go语言并发设计的核心。 说到底 goroutine 其实就是线程,但是它比线程更小,十几个 goroutine 可能体现在底层就是五六个线程,而且Go语言内部也实现了 goroutine 之间的内存共享。 使用 go 关键字就可以创建 goroutine,将 go 声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程,这种线程在Go语言中则被称为 goroutine。
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
接下来我们需要,用goroutine启动一个线程去完成hello任务:
如果执行了上述代码你就会发现,执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!。因为Go开启一个线程去执行hello任务之后,然后直接就print输出main goroutine done!,主进程就结束了,然后所属它的子线程也会被杀死。因为创建线程有时间开销,代码执行速度是非常快的。所以子线程还没来得及打印Hello Goroutine!就被杀死了。
Channel
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。goroutine之间通信通过channel,类似进程之间通信通过队列
channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
channel操作
1、发送:
ch <- 10 // 把10发送到ch中
2、接收
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
3、关闭
close(ch)
WaitGroup
WaitGroup 是一个计数信号量,可以用来记录并维护运行的 goroutine。如果 WaitGroup 的值大于 0,Wait 方法就会阻塞
可以看出输出并不是顺序的
并发安全LOCK
对于上述结果我们可以通过对输出前面加锁的操作完善
lock.lock() //上锁
lock.unlock() //去锁
依赖管理
依赖的概念:
在编写代码的过程中,很多时候不用自己去造轮子,会大量的使用第三方的库,这就可以称为依赖
依赖经历的三个阶段:
GOPATH
最初,Go 直接将依赖库源码扔进 GOPATH 的 src 文件夹以作为项目依赖。 GOPATH 是一个环境变量,指向一个目录,作为项目的编译产出目录和依赖目录。这是一个公共环境变量,也就意味着,所有项目都依赖于同一个 GOPATH,这就会导致这样的问题:如果项目 A 依赖于依赖库 Lib 的版本 1,而项目 B 依赖于同一个依赖库的版本 2,由于 GOPATH 并没有任何版本管理措施,就会导致编译出错。
Go Vendor
于是,Go 引入了 Go Vendor,通过在项目目录下新建 vendor 文件夹,并存放依赖库文件副本的方式,使得不同项目可以依赖不同的依赖库版本,解决了版本冲突的问题。 值得一提的是,如果无法在 vendor 文件夹中找到项目所需的依赖文件,那么 Go 会尝试回到 GOPATH 查找。 Go Vendor 的引入看似解决了版本问题,但是实际上依然造成了问题:如果项目 A 引入了 项目 B 和项目 C 作为依赖库,而后两者又共同依赖了项目 D 的不同版本,那么由于 B,C,D 作为项目 A 的依赖依然被同时存在同一个 vendor 文件夹中,依旧导致了依赖冲突。
Go Module
Go Module 通过项目路径中的 go.mod 文件(类似于 npm 的 pakcage.json 声明所需依赖的名称和版本范围),然后,通过 go.sum 文件记录项目实际使用的依赖和版本(类似于 npm 的 package-lock.json)。 我们没有必要像 Java 的 Maven/Gradle 那样手动编辑配置文件指定依赖,Go 为我们提供了 go get 和 go mod 两条指令来方便的添加和移除项目中的依赖。
依赖管理的三要素:
测试
单元测试:
Go语言中的测试依赖go test命令,并在后面加上-v后可以显示详细信息。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
通过设置测试函数对函数功能进行测试,将输出和预期结果进行对比,如果不正确则设置对应的错误级别和错误信息的方法叫单元测试。
接下来看一下测试例子:
如果测试通过即预期结果和执行结果一致可以得到PASS
测试覆盖率
通过go test -cover可以查看代码测试的覆盖的百分比,Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件,例如:
上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out文件中,然后我们执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。通常测试函数覆盖率要求100%,测试覆盖率60%
单元测试----依赖
总结
GO语言和其他语言以及操作系统的原理方面有很多相似且相通的地方,同时有自己的特点。
引用
【Go语言学习】——测试 (csdn)
【Go 并发】 | 菜鸟教程 (runoob.com)