GO协程和测试基础 | 青训营笔记

88 阅读10分钟

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

一. 本堂课重点内容

  1. go语言的并发优势
  2. goroutine和channel的定义,作用
  3. csp模型的作用,优势
  4. 对共享内存的安全访问-lock锁
  5. sync.WaitGroup的作用及使用方法
  6. go语言依赖管理的演进
  7. mod文件详解
  8. 依赖分发相关内容
  9. 测试(单元,mock,基准测试)
  10. 软件开发流程

二. 详细知识点介绍

2.1 go语言的并发优势

Go语言的并发和并行特性是它的优势之一,使得它能够在高性能和高并发环境中高效运行。

Go语言是一种并发编程语言,它提供了多种方法来轻松地管理并发和并行。它提供了 goroutines 和 channels 两种基本的并发机制。这些工具简化了编写并发代码的过程,并且在Go语言中实现了很好的并发性能。

2.2 goroutine和channel的定义,作用

Go语言中的 goroutine 和 channel 是 Go 编程中并发和并行的两个重要机制。

Goroutine 是 Go 语言的轻量级线程,它们可以在单个线程中并发执行。Goroutine 是由 Go 语言运行时管理的,使用起来比线程更简单。Goroutine 的实现方式相对于线程更加轻量级,所以可以创建更多的goroutine,这使得Go语言程序能够在更高的并发环境中运行。

协程是指在单个线程中执行的程序,它通过挂起和恢复来管理其执行状态。Go语言中的goroutine就是一种轻量级的协程。

Go语言中的channel是一种机制,用于在goroutines之间传递数据和进行同步。channel可以有缓冲和无缓冲两种。

无缓冲channel: 当channel没有缓冲时,在发送数据之前必须有一个接收者来接收数据。这种情况下,发送和接收操作是同步的,发送操作会阻塞直到有接收者接收数据。

有缓冲channel: 当channel有缓冲时,发送操作会将数据存储在缓冲区中,直到有接收者来接收数据。这种情况下,发送操作不会阻塞,只有当缓冲区满了之后才会阻塞。这样可以提高程序的并发性。

channel默认是无缓冲的,通过在make函数中设置第二个参数来创建有缓冲的channel,如:

ch := make(chan int, 100)

此时,ch是一个大小为100的int类型的有缓冲channel。

这些特性在Go语言中很好的支持了并发和并行, 使得程序能够在高并发环境中运行,并且更加简单易用。

2.3 csp模型的作用,优势

CSP (Communicating Sequential Processes) 是 Go 语言提倡的一种通信方式,它通过通信来共享内存,而不是通过共享内存来实现通信。

CSP的思想是, 通过通信来共享内存, 通过通信来协调并发操作. 也就是说, Go语言中的 goroutine 和 channel 是 CSP 模型的重要实现手段。在 CSP 模型中, goroutine 之间通过 channel 来进行通信,而不是通过共享内存。这样可以避免竞态条件和死锁等问题,并且使得程序更加可靠.

然而, Go语言并不是完全局限于CSP,它还保留了通过共享内存实现通信的方式,这样在一些场景下可能会更加高效. 但是, Go语言更鼓励使用CSP的方式来编写并发代码.

2.4 对共享内存的安全访问-lock锁

  1. 在并发编程中, lock(锁)是一种重要的同步机制, 用来保证在多个goroutines中对共享资源的访问是互斥的.

    Go语言中提供了 sync.Mutex 类型来实现锁机制。使用方法如下:

    import "sync"
    ​
    var mutex sync.Mutex
    ​
    // 代码块中使用锁
    func someFunc() {
        mutex.Lock()
        // 共享资源的访问
        // ...
        mutex.Unlock()
    }
    

    在上面的代码中, mutex.Lock() 会在进入代码块之前获取锁, mutex.Unlock() 会在退出代码块之后释放锁. 当一个goroutine获取了锁之后,其他goroutine将无法获取锁并访问共享资源,直到释放锁.

    sync.Mutex是互斥锁,另外,Go语言还提供了其他类型的锁来满足不同的并发场景,如:sync.RWMutex(读写锁)和sync.Once(只执行一次)等等.

    总之,使用锁能保证在并发环境下对共享资源的正确访问,防止数据竞争导致的不可预期错误.

2.5 sync.WaitGroup的作用及使用方法

sync.WaitGroup是Go语言中用来等待一组goroutine结束的工具。它可以跟踪一组goroutine的结束,并在所有的goroutine都结束后执行一些操作。

使用方法如下:

 import "sync"var wg sync.WaitGroup
 ​
 // 启动一个goroutine
 func someFunc() {
     wg.Add(1)
     go func() {
         // do some work
         wg.Done()
     }()
 }
 ​
 func main() {
     someFunc()
     someFunc()
     // 等待所有goroutine结束
     wg.Wait()
     // 所有goroutine结束后执行的操作
 }

在上面的代码中, 使用wg.Add(1)来跟踪一个goroutine的启动,使用wg.Done()来标记一个goroutine的结束. 最后使用wg.Wait()等待所有goroutine结束.

WaitGroup是一种计数器, 在开始时调用Add来指定有多少个goroutine需要等待, 然后在每个goroutine结束时调用Done来减少计数器, 最后调用Wait来等待所有goroutine结束.

WaitGroup比较适用于多个goroutine之间存在依赖关系的场景, 例如多个goroutine并行执行, 最后需要等待所有任务完成后才能继续进行下一步工作.

2.6 go语言依赖管理的演进

Go语言依赖管理的演进:

  • 初始阶段: Go语言刚开始没有内置的依赖管理机制,开发者需要手动下载和维护依赖包。
  • GOPATH时期: Go语言引入了GOPATH环境变量来管理依赖包,开发者可以通过go get命令来下载和管理依赖包。但是这种方式存在缺点,例如依赖版本控制困难、依赖冲突难以解决等。
  • Govendor时期: Govendor是一个第三方依赖管理工具,它通过在项目目录下创建vendor文件夹来管理依赖包。Govendor解决了GOPATH时期的一些问题,但是仍然存在一些限制。
  • Go module时期: Go 1.11版本引入了Go module作为官方依赖管理工具。Go module通过在项目目录下创建go.mod文件来管理依赖包,并且解决了Govendor时期的一些问题。

GOPATH的功能:

  • GOPATH是Go语言的工作环境变量, 它用于指定Go语言的工作目录, 用来管理Go语言的源码、二进制文件、库和依赖等.
  • GOPATH中有三个目录:
    • src目录用来存放源码文件,
    • pkg目录用来存放二进制文件,
    • bin目录用来存放可执行文件
  • GOPATH中的每个目录下都有一个子目录, 用来存放不同的包, 比如github.com, golang.org等.
  • go get命令可以用来下载和管理依赖包.

GOPATH的弊端:

  • GOPATH依赖管理存在版本控制困难, 依赖冲突难以解决等问题.
  • GOPATH不能很好地支持多个项目共享依赖.
  • GOPATH不能很好地处理多版本依赖.

Govendor的功能:

  • Govendor是一个第三方依赖管理工具, 它通过在项目目录下创建vendor文件夹来管理依赖包.
  • Govendor可以解决GOPATH时期的一些问题, 例如依赖版本控制、依赖冲突等.
  • Govendor提供了一些命令来管理依赖, 例如govendor fetch, govendor list等.
  • Govendor可以在不同的项目之间共享依赖包.

Govendor的弊端:

  • Govendor需要额外的配置和维护.
  • Govendor不能很好地处理多版本依赖.
  • Govendor不支持在线更新依赖包.

Go module的优势:

  • Go module是Go语言官方推出的依赖管理工具, 是一个标准化的解决方案.
  • Go module支持依赖版本控制, 可以解决依赖冲突问题.
  • Go module支持多版本依赖管理.
  • Go module支持在线更新依赖

2.7 mod文件详解

mod文件是Go module依赖管理工具用来维护依赖关系的文件。它由以下部分组成:

  • module:指定了当前项目所属的模块名称。
  • require:指定了当前项目所需要的依赖包及其版本。
  • exclude:指定了当前项目需要排除的依赖包及其版本。
  • replace:指定了当前项目需要替换的依赖包及其版本。

version详解:

  • 语义化版本: 使用通用的语义化版本格式, 如 v1.2.3, 指示主版本号、次版本号和修订版本号.
  • 基于commit伪版本: 使用特殊格式来指定某个模块的具体提交记录, 格式如 v0.0.0-20191010120000-abc
  • Incompatible 指定了当前项目不兼容的依赖包及其版本。 多个版本中选择一个最低的版本时,要求这个版本是所有版本中最稳定的,也是最能满足项目需求的版本。在 Go module 中,选择最低兼容版本的策略是在所有可用版本中选择一个最新的版本,这个版本会满足所有依赖项的要求,并且又不会导致不兼容性。这样做可以保证项目的稳定性和可维护性。

2.8 依赖分发相关内容

依赖分发是指在开发过程中,将依赖包从公共仓库下载到本地环境中使用。

回源是指当程序需要下载依赖包时,会从默认的回源地址下载。默认的回源地址是 Go module 的官方仓库地址,也可以自定义回源地址。

变量 GOPROXY 是 Go module 中的环境变量,用来指定 Go module 使用的代理服务器地址。如果设置了 GOPROXY 变量,Go module 就会使用指定的代理服务器来下载依赖包。这样可以加速下载速度,减少网络延迟,并且可以在防火墙内部使用。例如可以设置 GOPROXY 为 "goproxy.cn",或者"direct" (不使用代理)

简单来说, GOPROXY是Go module的代理设置, 用来指定依赖包的下载地址

2.9 测试(单元,mock,基准测试)

测试是保证代码质量的重要手段,在 Go 语言中也有丰富的测试工具支持。

单元测试:

  • 单元测试是对程序中独立的部分进行测试, 如函数、方法等.
  • 覆盖率是指单元测试覆盖了程序中多少代码.
  • 依赖:
    • 幂等: 对于相同的输入, 不管重复执行多少次, 结果都是相同的.
    • 稳定: 对于相同的输入, 结果也是相同的.

Mock:

  • 测试中常常需要模拟外部依赖, 例如数据库, 网络请求等.
  • 可以使用 mock 库来生成模拟对象, 来替代真实的外部依赖.

基准测试:

  • 基准测试是对程序性能进行测试, 用来验证程序的运行效率.
  • 可以使用testing包里的"testing.B"结构体进行基准测试, 例如testing.B.N,testing.B.ResetTimer,testing.B.StartTimer等

这些测试方法是 Go 语言测试工具提供的基本功能, 更多的测试工具和使用方法可以参考 Go 语言官方文档.

2.10 软件开发流程

"项目拆解->代码设计->测试运行" 是一种软件开发的流程。

  1. 项目拆解: 首先将项目拆分成小的模块, 明确每个模块的职责和功能.
  2. 代码设计: 根据每个模块的职责和功能, 设计模块之间的接口, 编写代码, 实现每个模块的功能.
  3. 测试运行: 测试项目中每个模块的代码, 确保它们按照预期运行, 并且修复错误, 提高代码的质量.

这种流程可以帮助我们更好地管理项目, 明确每个模块的职责, 提高代码的可维护性, 保证代码的质量.

三. 实践练习例子

本节课举了一个web服务的例子,库展示话题和回帖列表,具体内容这里不再详述

四. 课后个人总结

本节课着重学习了软件的开发流程以及协程的相关知识