Go语言入门-并发控制、单元测试、基准测试 | 青训营笔记

478 阅读4分钟

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

1.Goroutine引入

  • 当我们需要在父协程中等待子协程执行完毕后在往下执行,需要等待一段时间,而这段时间在不同环境下都可能不一致,最简单的方法就是预估一个较长的可以满足子协程执行完毕的时间来进行等待,如下代码,父协程睡眠一秒等待5个子协程执行完毕,所以接下来就是在go中实现多协程并发控制的几种方法的介绍
  • 优点:简单无脑
  • 缺点:无法准确等待子协程执行的时间,在业务更加复杂的情况下不可用,程序效率低下
func hello(i int) {
	println("hello goroutine : " + fmt.Sprint(i))
}

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

2. channel

2.1 基本语法

初始化channel:make(chan 元素类型, [缓冲大小])

无缓冲通道:make(chan int)

有缓冲通道:make(chan int, 2)

2.2 注意事项
  • 向一个nil channel发送信息会发生什么
    • 永久阻塞导致死锁,会发生fatal error: all goroutines are asleep - deadlock!
  • 从一个nil channel接收消息会发生什么
    • 永久阻塞导致死锁,会发生fatal error: all goroutines are asleep - deadlock!
  • 以上两种情况的fatal error都是在所有协程进入阻塞或睡眠状态才会发生的报错
  • 向一个已经关闭的channel发送信息会发生什么
    • 会直接发生panic:panic: send on closed channel
  • 从一个已经关闭的channel接收消息会发生什么
    • 可以正常接收值,<-channel中可以返回两个值,第一个为接收到的值,第二个代表是否正常接受数据,如果channel已经关闭,第一个为传输数据类型的零值,第二个为false
  • channel是同步的还是异步的
    • 有缓存的channel是异步的,没有缓存的channel为同步的,在没有缓存的channel中传输数据时需要注意发送数据时会发生阻塞,如果没有协程进行读取数据就会发生fatal dead lock
// sendNilChannel 向一个nil channel发送数据
func sendNilChannel() {
	var ch1 chan int

	go func() {
		time.Sleep(2 * time.Second)
		fmt.Println("test")
	}()
	var a int
    // 永远阻塞,当子协程执行完成报错
    // fatal error: all goroutines are asleep - deadlock!
	a = <-ch1
	fmt.Println(a)
}

// resvNilChannel 从一个nil channel中读取数据
func resvNilChannel() {
	var ch1 chan int

	go func() {
		time.Sleep(2 * time.Second)
		fmt.Println("test")
	}()
    // 永远阻塞,当子协程执行完成报错
    // fatal error: all goroutines are asleep - deadlock!
	ch1 <- 2
}

// sendClosedChannel 向一个已经关闭的channel发送数据
func sendClosedChannel() {
	ch1 := make(chan int)
	close(ch1)
    // 发生panic panic: send on closed channel
	ch1<-1
}

// rersvClosedChannel 从一个已经关闭的channel接收数据
func resvClosedChannel() {
	ch1 := make(chan int)
    go func() {
		v, ok := <-ch1
        // 2, true
		fmt.Println(v, ok)
		close(ch1)
        // 0, false
		v, ok = <-ch1
		fmt.Println(v, ok)
	}()

	ch1<-2
	time.Sleep(2 * time.Second)
}

// syncChannel 同步channel
func syncChannel() {
    // 不设置缓冲区大小默认为0
	ch1 := make(chan int)
	go func() {
		fmt.Println(<-ch1)
	}()
	ch1 <- 1
    // 为了子协程能够正常接收数据,睡眠一秒
	time.Sleep(1 * time.Second)
}

// asyncChannel 异步channel
func asyncChannel() {
    // 创建一个缓冲区大小为2的channel
	ch1 := make(chan int, 2)
    // 因为存在缓冲区,所以在缓冲区没满时不会发生阻塞,会继续往下执行
	ch1 <- 1
	ch1 <- 2
    // 如果channel缓冲区已满就会发生阻塞等待缓冲区有剩余空间
    // ch1 <- 3
    // 打印输出1/n2/n
	fmt.Println(<-ch1)
	fmt.Println(<-ch1)
}

3.Mutex

在Go中如果想要实现对一个变量进行并发安全地读写,就会使用到sync.Mutex互斥锁和sync.RWMutex读写锁

  • sync.Mutex互斥锁,同一时间只有一个协程能够访问临界区,加锁方法Lock(),解锁UnLock(),解锁一个已经解锁的锁会发生fatal error: sync: unlock of unlocked mutex
  • sync.RWMutex读写锁,读写互斥及支持多协程同时读,读锁加锁方法RLock(),读锁解锁方法RUnLock(),写锁加锁方法Lock(),解锁UnLock(),加了写锁之后不能加读锁,加写锁会阻塞并拒绝所有请求的读锁
    • 读锁可以加任意个

4.WaitGroup

方法:

  • Add(delta int)等待运行协程个数+delta个
  • Done()等待运行协程个数-1,通常在子协程中执行
  • Wait()等待直到运行协程个数为0

waitGroup传值会发生什么

// 正常执行
func n(wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("TEST2")
}

// 这时候的wg.Done()没有任何作用,协程执行完成后发生死锁报错,fatal error: all goroutines are asleep - deadlock!
// 在goland中会提示'p' passes a lock by the value: type 'sync.WaitGroup' contains 'interface{}' which is 'sync.Locker' 
// 就是这个原因导致waitGroup失效
func p(wg sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("TEST1")
}

5.单元测试

5.1 规则
  • 所有测试文件以_test.go结尾
  • 测试函数定义模板为func TestXXX(*testing.T)
  • 初始化逻辑放进TestMain()
func TestMain(m *testing.M) {
    // 测试前,数据装载、配置初始化等前置工作
    
    // 进行测试
    code := m.Run()
    
    // 测试完成,回收相关资源
    
    // 退出测试
    os.Exit(code)
}
5.2 判断用例返回结果是否与期望结果一致
  • 暴力法:直接使用==进行判断
  • 使用"github.com/stretchr/testify/assert"第三方包中的assert包提供的方法进行判断
5.3 单元测试覆盖率

单位测试覆盖率指的是单元测试函数对所测试的业务代码文件覆盖的行数比例

使用go test xxx_test.go xxx.go --cover即可查看到测试覆盖率

5.4 Mock测试

monkey包:github.com/bouk/monkey

  • 为一个函数打桩
  • 为一个方法打桩
  • Monkey Patch的作用域在Runtime,在运行时通过Go的unsafe包,能够将 内存中函数的地址替换为运行时函数的地址,将待打桩函数或方法的实现替换
  • 使用样例,通过patch对ReadFirstLine进行打桩mock,默认返回line110,通过defer卸载mock,这样将整个测试函数摆脱本地文件的束缚和依赖

6.基准测试

  • 基准测试以Benchmark开头,入参是testing.B,使用testing.B中的N值反复递增循环测试(对一个测试用例的默认测试时间是1秒,当测试用例函数返回还不到1秒,那么testing.B中的N值将按1、2、5、10、20、50...递增,并以递增后的值重新进行用例函数测试)
  • ResetTimer()重置计时器,在reset之前做了init或其他的准备操作,这些操作不应作为基准测试的范围
  • RunParallel()是多协程并发测试,协程个数取决于GOMAXPROCS,要增加非 CPU 绑定基准的并行度,请在 RunParallel 之前调用 SetParallelism()来改变协程个数