Go语言应用实践 | 青训营笔记

99 阅读9分钟

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

四、并发编程

4.1 并发&并行

image-20220519092829819

image-20220519092843677

4.2 Goroutine

image-20220519092932762

  • 线程(thread):用户态,轻量级线程,栈MB级别,是操作系统能够进行运算调度的最小单位,是进程中实际运作单位.一条线程指的是进城中一个单一顺序的控制流,一个进程可以并发多个线程,每个线程执行不同的任务.
  • 协程(Goroutine):内核态,线程跑多个协程,栈KB级别,其执行过程是不带返回值的函数调用.goroutine是go语言实现的轻量级线程实现,也就是每一个并发的执行单元.通过go func(){}的方式创建一个协程.

​ 在Go语言中,每一个并发的执行单元叫作一个goroutine。设想这里的一个程序有两个函数,一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻。马上就会看到这样的一个程序。

​ 当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。

/*
 * @Description:
 * @Author: zhuzhongzheng
 * @Date: 2022-05-19 09:55:54
 * @LastEditors: zhuzhongzheng
 * @LastEditTime: 2022-05-19 09:57:56
 */

package main

import (
	"fmt"
	"time"
)

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

func main() {
	for i := 0; i < 5; i++ {
		go func(j int) {
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)
}

/*
** 执行结果
hello goroutine : 3
hello goroutine : 1
hello goroutine : 4
hello goroutine : 0
hello goroutine : 2
*/
/*
 * @Description:
 * @Author: zhuzhongzheng
 * @Date: 2022-05-19 09:45:46
 * @LastEditors: zhuzhongzheng
 * @LastEditTime: 2022-05-19 09:50:05
 */

package main

import (
	"fmt"
	"time"
)

func main() {
	go spinner(100 * time.Millisecond) // 这里起一个goroutine与下面的函数同时执行,当main函数返回时,所有的goroutine都会结束.
	const n = 45
	fibN := fib(n) // slow
	fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

// 打印的动画
func spinner(delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c", r)
			time.Sleep(delay)
		}
	}
}

// 计算斐波那契数列
func fib(x int) int {
	if x < 2 {
		return x
	}
	return fib(x-1) + fib(x-2)
}

4.3 CSP

CSP(Communicating Sequential Processes)即交换消息的循序程序、通信顺序进程、交谈循序程序,用来描述并发性系统之间的交互的模型.

​ 在CSP模型里,进程间通过管道来进行通信.两个并发任务不需要共享内存,而是通过建立一条点对点的管道.

image-20220519100555744

4.4 Channels

channelsgoroutine之间的通信机制,一个channels就是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息.

​ channels分为有缓冲和无缓冲.

// 创建一个channels
ch := make(chan 元素类型,[缓冲大小])

// 声明
var ch chan 元素类型

// 无缓冲
ch := make(chan int)
// 有
ch := make(chan int, 2)

// 发送消息
通道变量 <- 值 // (值可以是变量、常量、表达式或者函数返回值等,类型必须和创建时的元素类型一致)

// 接收消息
data := <- ch // 阻塞接收
data,ok := <- ch // 非阻塞接收
// 循环接收
for data := range ch {
  
}

image-20220519101257443

/*
 * @Description:
 * @Author: zhuzhongzheng
 * @Date: 2022-05-19 09:19:01
 * @LastEditors: zhuzhongzheng
 * @LastEditTime: 2022-05-19 11:21:09
 */
package concurrence

/*
*	defer(延时执行语句),会将defer后的语句延迟执行,先被defer的语句最后执行,最后被defer的语句,最先被执行
 */
/*
*	func1 子协程发送0~9
* func2 子协程计算输出数字的平方
* 主写成输出最后的平方数
*/

func CalSquare() {
	// 创建一个无缓冲的channels
	src := make(chan int)
	// 创建一个有缓冲的channels
	dest := make(chan int, 3)

	// 创建一个goroutine
	go func() {
		// 延时执行
		defer close(src)
		for i := 0; i < 10; i++ {
			// 将 i 的值发送给 src channels
			println("func1", i)
			src <- i
		}
	}()
	go func() {
		// 延时执行
		defer close(dest)

		// 循环阻塞接收 将 src 里的值赋值给 i
		for i := range src {
			println("func2", i)
			// 将 i * i 的值发送gei dest channels
			dest <- i * i
		}
	}()
	// main gorouter 循环接收
	for i := range dest {
		//复杂操作
		println("main", i)
	}
}

4.5 锁

​ Go语言的sync包提供里两种锁类型:sync.Mutex(互斥锁)和sync.RWMutex(读写互斥锁)

  • Mutex是最简单的一种锁类型,当一个goroutine获取Mutex后,其他goroutine只能等待这个goroutine释放该Mutex.
  • RWMutex是经典的单写多读锁模型,在读锁占用的情况下,会阻止写,但不阻止读,也就是多个goroutine可同时获取读锁(调用RLock()方法);而写锁(调用Lock())会阻止任何其他goroutine进来,独占整个锁.
/*
 * @Description:
 * @Author: zhuzhongzheng
 * @Date: 2022-05-19 09:19:01
 * @LastEditors: zhuzhongzheng
 * @LastEditTime: 2022-05-19 11:41:17
 */
package concurrence

import (
	"sync"
	"time"
)

var (
	// 逻辑中使用的变量
	x int64
	// 与变量对应的使用的互斥锁
	lock sync.Mutex
)

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

// 对变量执行2000次+1操作,5个协程并发执行
func Add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("WithoutLock:", x)
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("WithLock:", x)
}

4.6 WaitGroup

​ go语言可以使用等待组来进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务,在sync.WaitGroup类型中,每个sync.WaitGroup类型中,每个sync.WaitGroup值在内部维护一个计数,默认为0.开始执行协程+1,结束协程-1,主协程

  • (wg *WaitGroup) Add(delta int):等待组的计数器+1
  • (wg *WaitGroup) Done(): 等待组的计数器-1
  • (wg *WaitGroup) Wait():当等待组计数器不等于0时阻塞直到变成0

func ManyGoWait() {
	// 声明一个等待组
	var wg sync.WaitGroup

	// 等待组 +5
	wg.Add(5)

	for i := 0; i < 5; i++ {
		// 开启一个并发goroutine
		go func(j int) {
			// 当函数执行完后,等待组 -1
			defer wg.Done()
			println(j)
		}(i)
	}
	// 等待所有任务执行完成
	wg.Wait()
	println("over")
}

五、依赖管理

5.1 依赖管理演进

​ go的依赖管理主要经历了三个阶段,到目前被广泛应用的是go module,主要解决两个问题:不同环境依赖的版本不同;哦你个值依赖版本.

image-20220519134218616

5.1.1 GOPATH

GOPATH是一个环境变量,是go的一个工作区,在go的早期版本里,需要将代码放在$GOPATH/src下,而go get产生的依赖也会自动下载到$GOPATH/src

GOPATH的弊端:当projectA依赖于某个包的v1版本,而projectB又依赖该包的v2版本,就是出现问题,无法实现package的多版本控制.

image-20220519135348358

5.1.2 Go Vender

​ 在每个项目都有一个vendor/目录来存放项目所需版本依赖的拷贝.依赖寻址的方式:首先查找vendor,再查GOPATH.

​ 弊端:同一个项目中的package可能依赖不同版本的包.

5.1.3 Go Module

Go Module是官方推出的依赖管理系统,go 1.16后默认开启,一般叫go mod,实现了定义版本规则和管理项目依赖关系

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

5.2 依赖管理

依赖管理的三要素

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

5.2.1 配置文件

​ 首先模块路径用来标识一个模块,从模块路径可以看出从哪里可以找到该模块,如果是github表示可以从github仓库找到该模块,如果项目的子包想单独被引用,需要单独的init go.mod文件进行管理. 往下是依赖的原生库.最下面是单元依赖.每个单元依赖用模块路径+版本号来唯一标识.

image-20220519142008629

go mod定义了版本规则,分为语义化版本和基于commit伪版本

  • 语义化版本:${MAJOR}.${MINOR}.${PATCH},不同的MAJOR表示不兼容的API,所以即使同一个库,MAJOR不同,也被认为是不同的模块.MINOR通常是新增函数或功能,向后兼容.PATCH一般是修复bug.
  • 基于commit伪版本:语义化版本-yyyymmddhhmmss-12位哈希前缀,每次提交commit后,go会默认生成一个伪版本号.

​ 依赖单元中的特殊标识符:

  • indirect后缀,表示go.mod对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖,表示间接依赖.
  • incompatible后缀

5.2.2 依赖分发

Go Proxy是一个服务站点,它会缓存源站中的软件内容,缓冲的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供immutabilityavilable的依赖分发.

image-20220519152702323

GOPROXY是一个Go Proxy站点URL列表,使用direct表示源站,会从列表的第一个开始,如果都没有,最终会到源站中直接下载依赖,缓存到proxy站点中.

image-20220519152740731

5.2.3 go get

​ go get命令可以借助代码管理工具通过远程拉取货更新代码包及其依赖包,并自动完成编译和安装.

get get example.org/pkg

@update 	默认
@none 		删除依赖
@v1.1.2		tag版本,语义版本
@23dfdd5	特定的commit
@master		分支

5.2.4 go mod

  • go mod init:创建空项目后,可以执行这个命令go mod init gitee.com/easyleroy/zzz.cn,指定后面的路径用来表示模块的标识和其他项目引用该模块时都会以import path作为共同的前缀
  • go mod download:下载依赖包
  • go mod tidy:拉取缺少的依赖,移除不用的依赖

六、测试

​ 测试是避免事故的最后一道屏障.测试主要分为三个测试模块:

  • 回归测试: 覆盖率最低,成本最高
  • 集成测试: 覆盖率中等,成本中等
  • 单元测试: 覆盖率最高,成本也是最低的

6.1 单元测试

​ 单元测试的主要流程是:给定输入、输出、测试单元(接口、函数、模块等),用结果与期望进行校对.

​ 单元测试的规则: 1.所有的测试文件以_text.go结尾;2.func TestXxx(*testing.T);3.初始化逻辑要放到TestMain

![image-20220519183509123](/Users/zhuzz/Library/Application Support/typora-user-images/image-20220519183509123.png)

6.1.1 例子

/*
 * @Description:
 * @Author: zhuzhongzheng
 * @Date: 2022-05-19 18:37:25
 * @LastEditors: zhuzhongzheng
 * @LastEditTime: 2022-05-19 18:55:27
 */

package main

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func HelloWorld() string {
	return "HelloWorld"
}

func TestHelloWorld(t *testing.T) {
	output := HelloWorld()
	expectOutput := "HelloWorld"

	assert.Equal(t, expectOutput, output)
}

6.1.2 覆盖率

go test 测试文件.go 需要测试的文件.go --cover 可以看到代码覆盖率.在实际项目中,一般要求50%~60%覆盖率,资金类项目要求80%,做单元测试.测试单元粒度足够小,函数单一职责.

6.1.3 依赖Mock

6.2 基准测试