golang_单元测试+协程管道

本文深入探讨了Go语言中的单元测试,包括使用testing框架进行测试用例编写、测试管理以及单元测试的重要性和应用场景。同时,文章详细介绍了Go的并发特性,如goroutine和channel,讨论了它们在多线程环境下的优势,以及如何通过channel解决goroutine间的通信问题。此外,还涵盖了协程中的错误处理和资源管理策略。
摘要由CSDN通过智能技术生成

单元测试

传统的方式来进行测试
在 main 函数中,调用 addUpper 函数,看看实际输出的结果是否和预期的结果一致,如果一致, 则说明函数正确,否则函数有错误,然后修改错误

//一个被测试函数
func addUpper(n int)  int {
	res := 0
	for i := 1; i <= n - 1; i++ {
		res += i
	}
	return res
}

func addUpper2(n int)  int {
	res := 0
	for i := 1; i <= n - 1; i++ {
		res += i
	}
	return res
}

func main() {
	//传统的测试方法,就是在main函数中使用看看结果是否正确
	// res := addUpper(10) // 1.+ 10 = 55
	// if res != 55 {
	// 	fmt.Printf("addUpper错误 返回值=%v 期望值=%v\n", res, 55)
	// } else {
	// 	fmt.Printf("addUpper正确 返回值=%v 期望值=%v\n", res, 55)
	// }
}

传统方法的缺点分析
1)不方便, 我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运 行,就可能去停止项目。
2)不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管 理和清晰我们思路
3)引出单元测试。-> testing 测试框架 可以很好解决问题。

单元测试-基本介绍

Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基 于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:
1)确保每个函数是可运行,并且运行结果是正确的
2)确保写出来的代码性能是好的,
3)单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决, 而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

单元测试-快速入门

package cal
import (
	"fmt"
	"testing" //引入go 的testing框架包
)

//编写要给测试用例,去测试addUpper是否正确
func TestAddUpper(t *testing.T) {

	//调用
	res := addUpper(10)
	if res != 55 {
		//fmt.Printf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n", 55, res)
		t.Fatalf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n", 55, res)
	}

	//如果正确,输出日志
	t.Logf("AddUpper(10) 执行正确...")

}

func TestHello(t *testing.T) {

	fmt.Println("TestHello被调用..")

}

单元测试快速入门总结

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

goroutine-快速入门

//编写一个函数,每隔1秒输出 "hello,world"
func test() {
	for i := 1; i <= 10; i++ {
		fmt.Println("tesst () hello,world " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

func main() {
	go test() // 开启了一个协程

	for i := 1; i <= 10; i++ {
		fmt.Println(" main() hello,golang" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

小结

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

设置CPU数量

func main() {
	cpuNum := runtime.NumCPU()
	fmt.Println("cpuNum=", cpuNum)

	//可以自己设置使用多个cpu
	runtime.GOMAXPROCS(cpuNum - 1)
	fmt.Println("ok")
}

channel管道

不同 goroutine 之间如何通讯
1)全局变量的互斥锁
2)使用管道 channel 来解决

使用全局变量加锁同步改进程序

因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
解决方案:加入互斥锁
我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)

// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.

var (
	myMap = make(map[int]int, 10)
	//声明一个全局的互斥锁
	//lock 是一个全局的互斥锁,
	//sync 是包: synchornized 同步
	//Mutex : 是互斥
	lock sync.Mutex
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	//这里我们将 res 放入到myMap
	//加锁
	lock.Lock()
	myMap[n] = res //concurrent map writes?
	//解锁
	lock.Unlock()
}

func main() {
	// 我们这里开启多个协程完成这个任务[200个]
	for i := 1; i <= 20; i++ {
		go test(i)
	}
	//休眠10秒钟【第二个问题 】
	//time.Sleep(time.Second * 5)
	//这里我们输出结果,变量这个结果
	lock.Lock()
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
	lock.Unlock()
}

为什么需要 channel

1)前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
2)主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。 3)如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作 状态,这时也会随主线程的退出而销毁
4)通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。 5) 上面种种分析都在呼唤一个新的通讯机制-channel

channel 的基本介绍

1)channle 本质就是一个数据结构-队列【示意图】
2)数据是先进先出【FIFO : first in first out】
3)线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
4)channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
channel 是引用类型
channel 必须初始化才能写入数据, 即 make 后才能使用 管道是有类型的,intChan 只能写入 整数 int

func main() {

	//演示一下管道的使用
	//1. 创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int, 3)

	//2. 看看intChan是什么
	fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)


	//3. 向管道写入数据
	intChan<- 10
	num := 211
	intChan<- num
	intChan<- 50
	// //如果从channel取出数据后,可以继续放入
	<-intChan
	intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量
	//4. 看看管道的长度和cap(容量)
	fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
	//5. 从管道中读取数据
	var num2 int
	num2 = <-intChan 
	fmt.Println("num2=", num2)
	fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))  // 2, 3
	//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
	num3 := <-intChan
	num4 := <-intChan
	//num5 := <-intChan
	fmt.Println("num3=", num3, "num4=", num4/*, "num5=", num5*/)
}

channel 使用的注意事项

1)channel 中只能存放指定的数据类型
2)channle 的数据放满后,就不能再放入了
3)如果从 channel 取出数据后,可以继续放入
4)在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

读写 channel 案例演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

channel 的遍历和关闭

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

	intChan := make(chan int, 3)
	intChan<- 100
	intChan<- 200
	close(intChan) // close
	//这是不能够再写入数到channel
	//intChan<- 300
	fmt.Println("okook~")
	//当管道关闭后,读取数据是可以的
	n1 := <-intChan
	fmt.Println("n1=", n1)

channel 的遍历
channel 支持 for–range 的方式进行遍历,请注意两个细节
1)在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
2)在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

	//遍历管道
	intChan2 := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan2<- i * 2  //放入100个数据到管道
	}

	//遍历管道不能使用普通的 for 循环
	// for i := 0; i < len(intChan2); i++ {

	// }
	//在遍历时,如果channel没有关闭,则会出现deadlock的错误
	//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
	close(intChan2)
	for v := range intChan2 {
		fmt.Println("v=", v)
	}

在这里插入图片描述

//write Data
func writeData(intChan chan int) {
	for i := 1; i <= 50; i++ {
		//放入数据
		intChan<- i //
		fmt.Println("writeData ", i)
		//time.Sleep(time.Second)
	}
	close(intChan) //关闭
}

//read data
func readData(intChan chan int, exitChan chan bool) {

	for {
		v, ok := <-intChan
		if !ok {
			break
		}
		time.Sleep(time.Second)
		fmt.Printf("readData 读到数据=%v\n", v) 
	}
	//readData 读取完数据后,即任务完成
	exitChan<- true
	close(exitChan)

}

func main() {

	//创建两个管道
	intChan := make(chan int, 10)
	exitChan := make(chan bool, 1)
	
	go writeData(intChan)
	go readData(intChan, exitChan)

	//time.Sleep(time.Second * 10)
	for {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}
}

应用实例

//向 intChan放入 1-8000个数
func putNum(intChan chan int) {

	for i := 1; i <= 80000; i++ {    
		intChan<- i
	}

	//关闭intChan
	close(intChan)
}

// 从 intChan取出数据,并判断是否为素数,如果是,就
// 	//放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {

	//使用for 循环
	// var num int
	var flag bool // 
	for {
		//time.Sleep(time.Millisecond * 10)
		num, ok := <-intChan //intChan 取不到..
		
		if !ok { 
			break
		}
		flag = true //假设是素数
		//判断num是不是素数
		for i := 2; i < num; i++ {
			if num % i == 0 {//说明该num不是素数
				flag = false
				break
			}
		}

		if flag {
			//将这个数就放入到primeChan
			primeChan<- num
		}
	}

	fmt.Println("有一个primeNum 协程因为取不到数据,退出")
	//这里我们还不能关闭 primeChan
	//向 exitChan 写入true
	exitChan<- true	

}

func main() {

	intChan := make(chan int , 1000)
	primeChan := make(chan int, 20000)//放入结果
	//标识退出的管道
	exitChan := make(chan bool, 8) // 4个



	start := time.Now().Unix()
	
	//开启一个协程,向 intChan放入 1-8000个数
	go putNum(intChan)
	//开启4个协程,从 intChan取出数据,并判断是否为素数,如果是,就
	//放入到primeChan
	for i := 0; i < 8; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}

	//这里我们主线程,进行处理
	//直接
	go func(){
		for i := 0; i < 8; i++ {
			<-exitChan
		}

		end := time.Now().Unix()
		fmt.Println("使用协程耗时=", end - start)

		//当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
		close(primeChan)
	}()


	//遍历我们的 primeChan ,把结果取出
	for {
		_, ok := <-primeChan
		if !ok{
			break
		}
		//将结果输出
		//fmt.Printf("素数=%d\n", res)
	}
	fmt.Println("main线程退出")
}

channel 使用细节和注意事项

1)channel 可以声明为只读,或者只写性质

	//1. 在默认情况下下,管道是双向
	//var chan1 chan int //可读可写
	//2 声明为只写
	var chan2 chan<- int
	chan2 = make(chan int, 3)
	chan2<- 20
	//num := <-chan2 //error
	fmt.Println("chan2=", chan2)
	//3. 声明为只读
	var chan3 <-chan int
	num2 := <-chan3
	//chan3<- 30 //err
	fmt.Println("num2", num2)

2)channel 只读和只写的最佳实践案例
在这里插入图片描述

  1. 使用 select 可以解决从管道取数据的阻塞问题
func main() {
	//使用select可以解决从管道取数据的阻塞问题
	//1.定义一个管道 10个数据int
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan<- i
	}
	//2.定义一个管道 5个数据string
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}
	//传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
	//问题,在实际开发中,可能我们不好确定什么关闭该管道.
	//可以使用select 方式可以解决
	//label:
	for {
		select {
			//注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
			//,会自动到下一个case匹配
			case v := <-intChan : 
				fmt.Printf("从intChan读取的数据%d\n", v)
				time.Sleep(time.Second)
			case v := <-stringChan :
				fmt.Printf("从stringChan读取的数据%s\n", v)
				time.Sleep(time.Second)
			default :
				fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
				time.Sleep(time.Second)
				return 
				//break label
		}
	}
}
  1. goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
    在这里插入图片描述
//函数
func sayHello() {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		fmt.Println("hello,world")
	}
}
//函数
func test() {
	//这里我们可以使用defer + recover
	defer func() {
		//捕获test抛出的panic
		if err := recover(); err != nil {
			fmt.Println("test() 发生错误", err)
		}
	}()
	//定义了一个map
	var myMap map[int]string
	myMap[0] = "golang" //error
}

func main() {
	go sayHello()
	go test()
	for i := 0; i < 10; i++ {
		fmt.Println("main() ok=", i)
		time.Sleep(time.Second)
	}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值