协程和测试 | 青训营笔记

105 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天,本次是针对于协程、测试(单元测试、mock测试、基准测试)的学习和理解。

协程与线程

协程:用户态,轻量级线程,栈MB级别

线程:内核态(系统态),线程跑多个协程,栈KB级别,,可以并发跑多个协程

go run其实是执行一个exe文件(windows),go build 入口是一个main包,有main包才能生产exe文件,一个mian包里只能有一个唯一的main方法

go语言开启协程

加上go关键字

package main
import (
  "fmt"
  "time"
)
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)
}
func main() {

  helloGoRoutine()

}

并行输出结果:

image.png

协程之间的通信---提倡通过共享内存实现通信

(goroutine是并发程序的执行体)

image.png

image.png

Channel(通道)--引用类型

创建--make(chan元素类型,[缓冲大小])

无缓冲通道 ---make(chan int)

image.png 发送的goroutine和接收的goroutine同步。

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

image.png

Channel---通过通信来实现共享内存

(eg:A子协程发送0-9数字,B子协程计算数字的平方,主协程输出最后的平方数)

package main

func CalSquare() {

  src := make(chan int)   //无缓冲通道

  dest := make(chan int, 3) //有缓冲通道,可以解决生产和消费速度问题

  go func() {

	//A协程
	defer close(src) //延迟资源关闭
      for i := 0; i < 10; i++ {
    	src <- i  //生产的数字发送到src通道
	}

  }()

  go func() {
      //B协程
      defer close(dest)
      for i := range src {   //通过src通道实现A协程和B协程的通信
    	dest <- i * i
      }
  }()

//主协程

  for i := range dest {
      println(i)    //通过src和dest能实现顺序性,是并发安全的
  }
}

func main() {

  CalSquare()

}

image.png

并发安全Lock----通过对临界资源访问的权限控制(加锁)

(eg:对变量执行2000次+1操作,5个协程并发执行)

package main

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

  }

}
func Add() {

  x = 0

  for i := 0; i < 5; i++ {

​    go addWithLock()

  }

  time.Sleep(time.Second)  //实现暴力阻塞

  println("withlock: ", x)



  x = 0

  for i := 0; i < 5; i++ {

​    go addWithoutLock()

  }

  time.Sleep(time.Second)

  println("withoutlock: ", x)

}



func main() {

  Add()

}

image.png

WaitGroup

包括:

Add(delta int)----------> 计数器+delta,开启协程的数量

Done()------------>计数器-1

Wait ()--------------->阻塞直到计数器为0(所有并发任务完成)

计数器:开启协程+1,执行结束-1,主协程阻塞直到计数器为0

package main

import(

  "fmt"

  "sync"

)

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


func ManyGoWait()  {
  var wg sync.WaitGroup
  wg.Add(5)   
  for i := 0; i < 5; i++ {
​    go func (j int)  {  //匿名函数defer wg.Done()

​      hello(j)

​    }(i)   //传值,将i的值作为实参传给形参j

  }

  wg.Wait()

}

func main()  {

  ManyGoWait()

}

image.png

依赖管理

go依赖管理演进:

GOPATH---->GO Vendor ---->Go Module

GOPATH

项目结构:

  • bin-----项目编译的二进制
  • pkg----项目编译的中间产物,加速编译
  • src-----项目源码

实现逻辑:在进行Go语言开发的时候,代码总是会保存在GOPATH/src目录下。在工程经过gobuildgoinstallgoget等指令后,会将下载的第三方包源代码文件放在GOPATH/src目录下。在工程经过go build、go install或go get等指令后,会将下载的第三方包源代码文件放在GOPATH/src目录下, 产生的二进制可执行文件放在 GOPATH/bin目录下,生成的中间缓存文件会被保存在GOPATH/bin目录下,生成的中间缓存文件会被保存在 GOPATH/pkg 下。

弊端:项目A 和项B 依赖于某一 package 的不同版本 (分别为 Pkg V1Pkg V2 ) 。而 src 下只能允许一个版本存在,那项目A 和项B 就无法保证都能编译通过。在 GOPATH 管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,无法做到不同项目依赖同一个库的不同版本。

image.png

两个项目依赖同一个package,两个版本v1,v2,在v2中没有实现兼容,可能覆盖了函数A。

GO Vendor

所有依赖包副本形式放在$ProjectRoot/vendor

依赖寻址方式:vendor => GOPATH(先在vendor下寻找,找不到再到GOPATH中寻找)

项目结构:

  • bin-----项目编译的二进制
  • pkg----项目编译的中间产物,加速编译
  • src-----项目源码
  • vendor

弊端:Vendor 无法很好解决依赖包版本变动问题和一个项目依赖同一个包的不同版本的问题。

image.png

项目C依赖项目B与项目A,项目A依赖项目D-V1,项目B依赖项目D-V2,这样底层同一个项目C底层依赖了同一个项目D的不同版本,一旦更新,又该如何指定不同的依赖版本则成了问题。

Go Module

Go Module 自 Go1.11 开始引入,Go 1.16 默认开启。

  • go.mod----配置文件,描述依赖
  • Proxy-----中心仓库管理依赖库
  • go get/mod---操作命令用于管理依赖库初始化、更新

module test // 依赖管理基本单元

go 1.18 // 原生库依赖原生的Go SDK版本

require ( // 单元依赖 标识命名:module路径 + 版本号 rsc.io/quote v1.5.2 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect rsc.io/sampler v1.3.0 // indirect )

依赖配置--version:

语义化版本

  • ${MAJOR}------不同的major间可以版本不兼容,实际是代码隔离的
  • ${MINOR}------通常做一些新增函数,需要保持前后兼容
  • ${PATH}------通常做一些代码bug修复

基于commit伪版本

依赖配置--indirect:

​ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect

A-->B-->C(A-->B直接依赖,A-->C间接依赖)

依赖配置--incompatible:

主版本2+模块会在模块路径增加/vN后缀。

对于没有go.mod文件并且主版本2+的依赖,会+incimpatible,标识出来可能存在不兼容的代码逻辑。

依赖分发--Proxy:

GOPROXY---服务站点,会缓存源站中的软件内容,可以保证依赖的稳定性。

GOPROXY="proxy1.cn,https://proxy2.cn,…

Proxy1---->Proxy2----->direct(表示源站)

测试

回归测试------终端回归一些主流场景

集成测试------对于系统功能测试

单元测试------对单独的函数、模块做测试(单元测试的覆盖率在一定程度上决定代码的质量)

从上到下,覆盖率逐层变大,成本却逐层降低。

单元测试

规则:

  • 所有测试文件以_test.go结尾
  • func TestXxxx(*testing.T)
  • 初始化逻辑放到了TestMain中
func TestMain(m *testing.M){

    //测试前:数据装载,配置初始化等前置工作

    code :=m.Run()

    //测试后:释放资源等收尾工作

    os.Exit(code)
}

------unittest.go-----

package class21

func HelloTom() string {

  return "Jerry"

}

------unittest_test.go-----

package class21

import (
  "testing"

)


func TestHelloTom(t *testing.T) {

  output := HelloTom()

  expectOut := "Tom"

  if output != expectOut {

​    t.Errorf("Expected %s do not match actual %s", expectOut, output)

  }

}

image.png

使用assert

导入assert包:go get "github.com/stretchr/testify/assert"

package class21



import (

  "github.com/stretchr/testify/assert"

  "testing"

)


func TestHelloTom(t *testing.T) {

  output := HelloTom()

  expectOut := "Tom"

  assert.Equal(t, expectOut, output)  //expectOut是期望值,output是实际值

}

image.png

覆盖率------对单元测试的评估

---------coverage.go---------------

package class21

func JudgeScore(score int16) bool {

  if score >= 60 {  //执行return true  //执行  ,执行语句共2,总共3句,所以代码覆盖率为2/3=66.7%

  }

  return false  //未执行

}

---------coverage_test.go---------------

package class21



import (

  "github.com/stretchr/testify/assert"

  "testing"

)

func TestJudgeScore(t *testing.T) {

  isPass := JudgeScore(70)

  assert.Equal(t, true, isPass)

}

image.png

必须在当前运行go文件所在目录下,即class2_1

查看代码覆盖率:go test coverage_test.go coverage.go --cover

  • 一般覆盖率:50%-60%,较高覆盖率80%以上
  • 测试分支相互独立,全面覆盖(不重不漏)
  • 测试单元粒度足够小,要求函数足够小,函数单一职责

依赖

外部依赖(强依赖)-----File、DB、Cache

单元测试目标:

  • 幂等----重复运行一个测试,每次的结果是一样的
  • 稳定---单元测试能够相互隔离的,能在任何时间任何函数,独立地运行

Mock测试

如果简单的单元测试,别人一旦修改文件,测试结果就改变了,对相应的场景有强依赖:

-----log.txt-----

image.png

-----file.go-----

package class21


import (

  "bufio"

  "os"

  "strings"

)


func ReadFirstLine() string {

  open, err := os.Open("log.txt")

  defer open.Close()

  if err != nil {

​    return ""

  }

  scanner := bufio.NewScanner(open)

  for scanner.Scan() {

​    return scanner.Text()

  }

  return ""

}



func ProcessFirstLine() string {

  line := ReadFirstLine()

  destline := strings.ReplaceAll(line, "11", "00")

  return destline

}

-----file_test.go-----

package class21

import (

  "github.com/stretchr/testify/assert"

  "testing"

)


func TestProcessFirstLine(t *testing.T) {

  firstLine := ProcessFirstLine()

  assert.Equal(t, "line00", firstLine)

}

image.png

mock常用测试包之一:monkey-----github.com/bouk/monkey

为一个函数打桩----一个函数A去替换一个函数B,B是原函数,A是打桩函数

为一个方法打桩

如果安装时执行 go get github.com/bouk/monkey

image.png

根据提示,安装时执行 go get bou.ke/monkey

导入时:import "bou.ke/monkey"

package class21


import (

  // "github.com/bouk/monkey"

  "bou.ke/monkey"

  "github.com/stretchr/testify/assert"

  "testing"

)


func TestProcessFirstLineWithMock(t *testing.T) {

  monkey.Patch(ReadFirstLine, func() string {  //对ReadFirstLine做打桩操作,默认一直输出"line110"return "line110"

  })

  defer monkey.Unpatch(ReadFirstLine)  //做桩函数的卸载

  line := ProcessFirstLine()

  assert.Equal(t, "line000", line)

}

image.png

基准测试

优化代码,对当前代码分析

  • 基准测试的代码文件比如以_test.go结尾
  • 基准测试函数必须以Benchmark开头,必须是可导出的
  • 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
  • 基准测试函数不能有返回值
  • b.ResetTimer是重置计时器,避免for循环之前的初始化代码干扰
  • 被测试代码要放入最后的for循环中
  • b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能
package class21


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)]

}

-----------base_test.go-----------

package class21


import (

  "testing"

)



func BenchmarkSelect(b *testing.B) {

  InitServerIndex()

  b.ResetTimer() //定时器重置

  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()

​    }

  })

}

串行测试结果:

image.png

并行测试结果:

image.png

优化

使用fastrand-----go get github.com/NebulousLabs/fastrand

出现错误:

image.png

解决方法:

(1)在GOPATH的src文件夹中创建以下路径:"D:\go_workspace\src\golang.org\x"

(2)执行以下语句

mkdir -p $GOPATH/src/golang.org/x
cd $GOPATH/src/golang.org/x
git clone https://github.com/golang/sync.git
git clone https://github.com/golang/crypto.git
git clone https://github.com/golang/sys.git

(3)再次执行go get github.com/NebulousLabs/fastrand

package class21

import (

  "github.com/NebulousLabs/fastrand"

)

var ServerIndex [10]int

func InitServerIndex() {

  for i := 0; i < 10; i++ {

​    ServerIndex[i] = i + 100

  }

}


// 随机选择服务器

func FastSelect() int {

  return ServerIndex[fastrand.Intn(10)]   //fastrand失去了随机数列的一致性

}
package class21



import (

  "testing"

)



func BenchmarkSelect(b *testing.B) {

  InitServerIndex()

  b.ResetTimer() //定时器重置

  for i := 0; i < b.N; i++ {

​    // Select() //做串行的压力测试

​    FastSelect()

  }

}



func BenchmarkSelectParallel(b *testing.B) {

  InitServerIndex()

  b.ResetTimer()

  b.RunParallel(func(pb *testing.PB) {

​    for pb.Next() {
​      FastSelect()  

​    }

  })

}

image.png