Golang单元测试、go协程和管道示例详解

Written by

in

文章目录
  • 到此这篇关于Golang单元测试、go协程和管道的文章就介绍到这了,更多相关Go单元测试、go协程和管道内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客! 您可能感兴趣的文章: Golang channle管道的基本使用及快速入门 golang channel管道使用示例解析 Golang 协程配合管道的实现示例 Go 并发实现协程同步的多种解决方法 go获取协程(goroutine)号的实例 golang协程与线程区别简要介绍 go语言中的协程详解 浅谈go 协程的使用陷阱
  • 目录
    • 1.单元测试
      • 1.1基本介绍
      • 1.2单元测试编写步骤
      • 1.3总结
      • 1.4单元测试综合案例
    • 2.goroutine
      • 2.1进程和线程说明
      • 2.2进程线程关系示意图
      • 2.3并发和并行
      • 2.4Go协程和Go主线程
      • 2.5 goroutine使用案例
      • 2.6MPG基本介绍
      • 2.7设置Go运行cpu的个数
    • 3.channel(管道)
      • 3.1全局变量+互斥锁解决资源竞争
      • 3.2channel的介绍
      • 3.3channel的使用案例
      • 3.4chan的关闭和遍历
        • 3.4.1chan的关闭
        • 3.4.3chan的遍历
      • 3.5goroutine和channel结合
        • 3.6管道阻塞的机制
          • 3.7channel使用细节
          • 结语

            Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,testing框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。

            通过单元测试,可以解决如下问题:

            1. 确保每个函数是可运行,并且运行结果是正确的
            2. 确保写出来的代码性能是好的
            3. 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

            (1)创建测试文件

            测试文件以*test.go结尾,例如:math_test.go

            (2)编写测试函数

            测试函数以Test开头,接受*testing.T参数

            func TestAdd(t *testing.T){
            	//测试逻辑
            }

            (3)表格驱动测试

            使用结构体切片定义多个测试用例,循环遍历执行

            func TestAdd(t *testing.T){
            	//使用结构体切片定义多个测试用例
            	tests:=[]struct{
            		a,b,want int
            	}{
            		{1,2,3},
            		{3,4,7},
            		{5,6,11},
            	}
            	//循环遍历执行
            	for _,v:=range tests{
            		sum:=Add(v.a,v.b)
            		if sum != v.want{
            			t.Errorf("Add(%d,%d)=%d;want %dn",v.a,v.b,sum,v.want)
            		}
            	}
            }
            

             (4)子测试

            使用t.Run()分组测试用例,提升可读性和选择性运行

            func TestAdd(t *testing.T){
            	//使用结构体切片定义多个测试用例
            	tests:=[]struct{
            		name string
            		a,b,want int
            	}{
            		//分组
            		{"正数",1,2,3},
            		{"负数",-3,-4,-7},
            		{"零值",0,0,0},
            	}
            	//循环遍历执行
            	for _,v:=range tests{
            		sum:=Add(v.a,v.b)
            		if sum != v.want{
            			t.Errorf("Add(%d,%d)=%d;want %dn",v.a,v.b,sum,v.want)
            		}
            	}
            }

            (5)错误测试

            验证函数是否返回预期错误

            func TestDivide(t *testing.T){
            	_,err:=Divide(6,0)
            	if err ==nil{
            		t.Fatal("预期错误,并未返回")
            	}
            	if err.Error()!="除零错误"{
            		t.Error("错误消息不符:%s",err)
            	}
            }

            (6)测试覆盖率

            生成并查看覆盖率报告

            go test -coverprofile=coverage.out
            go tool cover -html=coverage.out

            (7)初始化和清理

            使用TestMain进行全局设置

            func TestMain(m *testing.M){
            	setup()
            	code:=m.Run()
            	teardown()
            	os.Exit(code)
            }

            (8)使用t.Cleanup

            注册清理函数

            func TestDB(t *testing.T){
            	db:=setupDB()
            	t.Cleanup(func(){
            		teardownDB(db)
            	})
            	//测试逻辑
            }

            (9)模拟依赖

            通过接口实现Mock()

            type Storage interface{
            	Get(id int) string
            }
            
            type MockStorage struct{}
            func (m*MockStorage)Get(id int)string{
            	return "mock"
            }
            
            func TestService(t*testing.T){
            	s:=&Service{storage:&MockStorage{}}
            	//测试逻辑
            }

            (10)跳过测试

            func TestNetwork(t *testing.T){
            	if testing.Short(){
            		t.Skip("短模式下跳过")
            	}
            	//网络相关测试
            }
            

            (11)并行测试

            使用t.Parallel()加速测试

            func TestParallel(t *testing.T){
            	t.Parallel()
            	//并发安全测试逻辑
            }

            1. 测试用例文件名必须以 test.go结尾。比如 cal tert.go,cal 不是固定的。
            2. 测试用例函数必须以Test开头,一般来说就是Test+被测试的函数名,比如TestAddUpper。TestAddUpper(t *tesing.T)的形参类型必须是 *testing.T【看一下手册】
            3. 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper
            4. Testsub运行测试用例指令                                                                                        (1)cmd>go test[如果运行正确,无日志,错误时,会输出日志]                            (2)cmd>gotest-v[运行正确或是错误,都输出日志]
            5. 当出现错误时,可以使用t.Fatalf来格式化输出错误信息,并退出程序
            6. t.Logf方法可以输出相应的日志
            7. 测试用例函数,并没有放在main函数中,也执行了,这就是测试用例的方便之处
            8. PASS表示测试用例运行成功,FAIL表示测试用例运行失败

            被测代码文件:mathutil.go

            package main
            
            // 计算两个数的和
            func Add(a, b int) int {
                return a + b
            }
            
            // 计算阶乘
            func Factorial(n int) int {
                if n < 0 {
                    return -1
                }
                if n == 0 {
                    return 1
                }
                return n * Factorial(n-1)
            }
            
            // 判断是否为素数
            func IsPrime(n int) bool {
                if n < 2 {
                    return false
                }
                for i := 2; i*i <= n; i++ {
                    if n%i == 0 {
                        return false
                    }
                }
                return true
            }

            测试文件:mathutil_test.go

            package main
            
            import (
                "testing"
            )
            
            func TestAdd(t *testing.T) {
                tests := []struct {
                    a, b, expected int
                }{
                    {1, 2, 3},
                    {-1, 1, 0},
                    {0, 0, 0},
                    {100, 200, 300},
                }
            
                for _, tt := range tests {
                    result := Add(tt.a, tt.b)
                    if result != tt.expected {
                        t.Errorf("Add(%d, %d) = %d; expected %d", tt.a, tt.b, result, tt.expected)
                    }
                }
            }
            
            func TestFactorial(t *testing.T) {
                tests := []struct {
                    n, expected int
                }{
                    {0, 1},
                    {1, 1},
                    {5, 120},
                    {10, 3628800},
                    {-1, -1},
                }
            
                for _, tt := range tests {
                    result := Factorial(tt.n)
                    if result != tt.expected {
                        t.Errorf("Factorial(%d) = %d; expected %d", tt.n, result, tt.expected)
                    }
                }
            }
            
            func TestIsPrime(t *testing.T) {
                tests := []struct {
                    n        int
                    expected bool
                }{
                    {2, true},
                    {3, true},
                    {4, false},
                    {17, true},
                    {1, false},
                    {0, false},
                    {-1, false},
                }
            
                for _, tt := range tests {
                    result := IsPrime(tt.n)
                    if result != tt.expected {
                        t.Errorf("IsPrime(%d) = %v; expected %v", tt.n, result, tt.expected)
                    }
                }
            }

            运行:在bash下运行

            go test -v

            • 进程就是程序程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
            • 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
            • 一个进程可以创建核销毁多个线程,同一个进程中的多个线程可以并发执行。
            • 一个程序至少有一个进程,一个进程至少有一个线程

            1. 多线程程序在单核上运行,就是并发
            2. 多线程程序在多核上运行,就是并行
            • 并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
            • 并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行 

            Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。

            Go协程的特点

            • 有独立的栈空间
            • 共享程序
            • 堆空间调度由用户控制
            • 协程是轻量级的线程

            使用 go 关键字启动协程

            package main
            import(
            	"fmt"
            	"time"
            )
            
            func test(){
            	for i:=0;i<10;i++{
            		fmt.Printf("test()~%vn",i)
            		time.Sleep(1000*time.Millisecond)//休眠一秒
            	}
            }
            
            func main(){
            	//启动协程
            	go test()
            
            	for i:=0;i<5;i++{
            		fmt.Printf("main()~%vn",i)
            		time.Sleep(1000*time.Millisecond)//休眠一秒
            	}
            }

            说明:

            • 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源
            • 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
            • Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了
            • 当主线程终止时,其他的协程也将终止

            1. M 代表着一个内核线程,也可以称为一个工作线程。goroutine就是跑在M之上的
            2. P 代表着(Processor)处理器 它的主要用途就是用来执行goroutine的,一个P代表执行一个Go代码片段的基础(可以理解为上下文环境),所以它也维护了一个可运行的goroutine队列,和自由的goroutine队列,里面存储了所有需要它来执行的goroutine。
            3. G 代表着goroutine 实际的数据结构(就是你封装的那个方法),并维护者goroutine 需要的栈、程序计数器以及它所在的M等信息。
            4. Seched 代表着一个调度器 它维护有存储空闲的M队列和空闲的P队列,可运行的G队列,自由的G队列以及调度器的一些状态信息等。

            所用到的方法:

             案例:

            package main
            import (
            	"fmt"
            	"runtime"
            )
            
            func main(){
            	//获取当前(逻辑)cpu的数量
            	num:=runtime.NumCPU()
            	//设置num-1的cpu运行go程序
            	runtime.GOMAXPROCS(num)
            	fmt.Println(num)
            }

            案例: 启动20个协程求1~20的阶乘

            package main
            import (
            	"fmt"
            	"sync"
            	"time"
            )
             
            var (
            	myMap =make(map[int]int,10)
            	//声明一个全局的互斥锁
            	lock sync.Mutex
            )
            
            func test(n int){
            	res:=1
            	for i:=1;i<=n;i++{
            		res*=i
            	}
            	//这里我们将res放入myMap中
            	//加锁
            	lock.Lock()
            	myMap[n]=res
            	//解锁
            	lock.Unlock()
            }
            
            func main(){
            	//开启多个协程完成求阶乘
            	for i:=1;i<=20;i++{
            		go test(i)
            	}
            
            	//休眠5秒
            	time.Sleep(5*time.Second)
            	//加锁
            	lock.Lock()
            	for i,v:=range myMap{
            		fmt.Printf("map[%d]=%dn",i,v)
            	}
            	//解锁
            	lock.Unlock()
            	 
            }

            1. channel本质就是一个数据结构-队列
            2. 数据是先进先出[FIFO]
            3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
            4. channel是类型安全的,只能发送和接收指定类型的数据,例如:一个string的channel只能存放string类型数据
            5. 无缓冲channel是同步的,发送和接收操作会阻塞直到另一端准备好
            6. 缓冲channel允许在没有接收者的情况下发送有限数量的数据
            7. channel可以通过 close() 函数关闭,关闭后不能再发送数据

             (1)定义channel

            var 名字 chan 类型 

            (2)创建channel

            ch := make(chan int) // 创建一个无缓冲的int类型channel
            ch := make(chan string, 10) // 创建一个缓冲大小为10的string类型channel

            (3)发送和接受数据

            ch <- 42 // 发送数据到channel
            value := <-ch // 从channel接收数据
            <-ch    //也可以不接受数据,将数据推出

            (1)基本数据类型Chan

            package main
            import (
            	"fmt"
            )
            
            func main(){
            	//定义一个接受int类型的channel
            	var intChan chan int
            	//使用channel前需要make
            	intChan=make(chan int,4)
            
            	//进channel
            	intChan<-1
            	intChan<-2
            	intChan<-3
            	intChan<-4
            	// intChan<-5  注意,当我们给channel写入数据时,不能超过其容量
            
            	//看看intChan是什么
            	fmt.Printf("intChan 的值是%v,地址为%p,长度为%v,容量为%vn",intChan,&intChan,len(intChan),cap(intChan))
            	
            	//从channel中取数据
            	var num int
            	num=<-intChan
            	fmt.Println("num=",num)
            	fmt.Printf("intChan 的长度为%v,容量为%vn",len(intChan),cap(intChan))
            
            	//注意:在没有使用协程的情况下,如果我们的管道数据已经全部取出,继续取数据就会报deadlock
            	num2:=<-intChan
            	<-intChan  //数据出channel 也可以不接受,相当于丢弃
            	num3:=<-intChan
            
            	fmt.Printf("num2=%d , num3=%dn",num2,num3)
            	//num2=2 , num3=4
            	//数据出channel时先进先出的
            }

            (2)struct管道和map管道:

            package main
            import (
            	"fmt"
            )
            
            type Cat struct{
            	Name string
            	Age int
            }
            
            func main(){
            	//structChan
            	cat1:=Cat{"jav",12}
            	cat2:=Cat{"mimi",3}
            	var catChan chan Cat
            	catChan=make(chan Cat,3)
            	catChan<-cat1
            	catChan<-cat2
            	fmt.Println(<-catChan)
            	cat3:=<-catChan
            	fmt.Println(cat3)
            
            	//mapChan
            	var mapChan chan map[string]string
            	mapChan=make(chan map[string]string,5)
            	m1:=make(map[string]string,10)
            	m1["功法1"]="九幽玄天"
            	m1["功法2"]="天罡决"
            	mapChan<-m1
            
            	m2:=make(map[string]string)
            	m2["神通1"]="古神决"
            	m2["神通2"]="呼风唤雨"
            	mapChan<-m2
            	//输出
            	fmt.Println(<-mapChan)
            	fmt.Println(<-mapChan)
            }

            (3)interface{}类型的chan

            package main
            import(
            	"fmt"
            )
            
            type Person struct{
            	Name string
            	Age int
            }
            type Dog struct{
            	Name string
            	Age int
            }
            
            func main(){
            	//能接受任意类型的管道
            	var allChan chan interface{}
            	allChan=make(chan interface{},10)//不指定空间10则无法使用
            	//定义变量
            	num1:=2
            	str1:="channel is interesting"
            	person1:=Person{"李星云",21}
            	dog1:=Dog{"小红",4}
            	//chan接受数据
            	allChan<-num1
            	allChan<-str1
            	allChan<-person1
            	allChan<-dog1
            
            	//取数据
            	num2:=<-allChan
            	str2:=<-allChan
            	person2:=<-allChan
            	dog2:=<-allChan
            	//打印
            	fmt.Printf("num2的类型为%T,值为%vn",num2,num2)
            	fmt.Printf("str2的类型为%T,值为%vn",str2,str2)
            	fmt.Printf("person2的类型为%T,值为%vn",person2,person2)
            	fmt.Printf("dog2的类型为%T,值为%vn",dog2,dog2)
            
            	//验证
            	// fmt.Println(person2.Name)
            	//会报错,此时是interface{}类型,不能由属性
            	//需类型断言解决该问题
            	person3,ok:=person2.(Person)
            	if ok==false{
            		fmt.Println("类型断言失败")
            	}
            	fmt.Printf("person3的类型为%T,值为%vn",person3,person3)
            	fmt.Println(person3.Name) 
            }

            用到的函数:

            使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据

            案例:

            package main
            import (
            	"fmt"
            )
            
            func main(){
            	var intChan chan int
            	intChan=make(chan int,5)
            	intChan<-1
            	intChan<-2
            	intChan<-3
            	//关闭管道
            	close(intChan)
            	//这时不能再写数据到intChan
            	//intChan<-4
            	//panic: send on closed channel
            	//但是可以读数据
            	fmt.Println(<-intChan)
            }

            channel支持for–range的方式进行遍历,

            请注意两个细节:

            1. 在遍历时,如果channel没有关闭,则回出现deadlock的错误
            2. 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

            案例1:没有close

            package main
            import (
            	"fmt"
            )
            
            func main(){
            	var intChan chan int =make(chan int,100)
            
            	//遍历chan,接受数据
            	for i:=0;i<100;i++{
            		intChan<-i*2
            	}
            
            	//在遍历读数据时,如果channel没有关闭,
            	//则会出现fatal error: all goroutines are asleep - deadlock!
            	for v:=range intChan{
            		fmt.Println(v)
            	}
            	fmt.Println("遍历完成~")//不会输出
            }

             案例2:close

            package main
            import (
            	"fmt"
            )
            
            func main(){
            	var intChan chan int =make(chan int,100)
            
            	//遍历chan,接受数据
            	for i:=0;i<100;i++{
            		intChan<-i*2
            	}
            
            	//在遍历读数据时,如果channel已经关闭,
            	//则会正常遍历数据,遍历完数据后就会退出遍历
            	close(intChan)
            	for v:=range intChan{
            		fmt.Println(v)
            	}
            	fmt.Println("遍历完成~")//正常输出
            }

            package main
            import (
            	"fmt"
            	_ "time"
            )
            
            //writer Data
            func writeData(intChan chan int){
            	for i:=1;i<=200;i++{
            		//放入数据
            		intChan<-i 
            		fmt.Println("writeData.............写入数据:",i)
            	}
            	//关闭
            	close(intChan)
            }
            
            //read Data
            func readData(intChan chan int,exitChan chan bool){
            	for{
            		v,ok:=<-intChan
            		if !ok{
            			break
            		}
            		fmt.Printf("readData 读到数据=%vn",v)
            	}
            	//读完数据
            	exitChan<-true
            	close(exitChan)
            }
            
            
            func main(){
            	//创建两个管道
            	intChan := make(chan int,200)
            	exitChan := make(chan bool,1)
            
            	go writeData(intChan)
            	go readData(intChan,exitChan)
            
            	// time.Sleep(5*time.Second)
            	for {
            		_,ok:=<-exitChan
            		if !ok{
            			break
            		}
            	}
            }

            (1)无缓冲管道 :

            • 发送操作会阻塞,直到有另一个goroutine执行接收操作
            • 接收操作会阻塞,直到有另一个goroutine执行发送操作
            • 这种机制实现了goroutine之间的同步

            (2)有缓冲管道 :

            • 当缓冲区未满时,发送操作不会阻塞
            • 当缓冲区为空时,接收操作会阻塞
            • 当缓冲区满时,发送操作会阻塞
            • 这种机制允许一定程度的异步操作

            案例:

            package main
            
            import (
            	"fmt"
            	"time"
            )
            
            func main() {
            	ch := make(chan int) // 无缓冲管道
            
            	go func() {
            		time.Sleep(2 * time.Second)
            		fmt.Println("准备发送数据")
            		ch <- 42
            		fmt.Println("数据已发送")
            	}()
            
            	fmt.Println("等待接收数据")
            	value := <-ch
            	fmt.Println("接收到数据:", value)
            }

            解除阻塞的方式 :

            • 对于发送操作:有goroutine执行接收操作
            • 对于接收操作:有goroutine执行发送操作
            • 使用context或timeout机制取消阻塞
            • 关闭管道(关闭后接收操作不会阻塞,会立即返回零值)

            解除阻塞案例:

            package main
            
            import (
            	"fmt"
            	"time"
            )
            
            func main() {
            	ch := make(chan int)
            
            	// 启动一个goroutine,延迟发送数据
            	go func() {
            		time.Sleep(2 * time.Second)
            		ch <- 42
            		fmt.Println("数据已发送")
            	}()
            
            	// 主goroutine尝试接收数据,会阻塞直到有数据
            	fmt.Println("等待接收数据...")
            	value := <-ch
            	fmt.Println("接收到数据:", value)
            
            	// 使用select实现超时解除阻塞
            	select {
            	case v := <-ch:
            		fmt.Println("接收到数据:", v)
            	case <-time.After(1 * time.Second):
            		fmt.Println("超时,解除阻塞")
            	}
            
            	// 关闭管道解除阻塞
            	close(ch)
            	v, ok := <-ch
            	fmt.Println("从关闭的管道接收:", v, "是否成功:", ok)
            }

            • 避免向已关闭的channel发送数据,这会导致panic
            • 重复关闭channel会导致panic
            • 从已关闭的channel接收数据会立即返回零值
            • 使用nil channel会永久阻塞
            • 注意channel的缓冲区大小,过大会占用过多内存
            • 使用 select 语句可以同时处理多个channel操作
            • 对于只读或只写channel,可以使用类型约束( <-chan 和 chan<- )
            • 使用 range 可以遍历channel直到它被关闭

            到此这篇关于Golang单元测试、go协程和管道的文章就介绍到这了,更多相关Go单元测试、go协程和管道内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!

            您可能感兴趣的文章:

            • Golang channle管道的基本使用及快速入门
            • golang channel管道使用示例解析
            • Golang 协程配合管道的实现示例
            • Go 并发实现协程同步的多种解决方法
            • go获取协程(goroutine)号的实例
            • golang协程与线程区别简要介绍
            • go语言中的协程详解
            • 浅谈go 协程的使用陷阱

            站内搜索