Golang第十篇:单元测试、go协程与通道解析
1. 单元测试
1.1 基础阐述
Go语言自带轻量级测试框架testing
以及go test
命令来开展单元测试与性能测试。该测试框架和其他语言的测试框架类似,可基于它编写针对相应函数的测试用例,也能编写压力测试用例。通过单元测试,能达成以下目标:
1. 确保每个函数可正常运行且结果正确;
2. 保证所编写代码性能良好;
3. 及时发现程序设计或实现中的逻辑错误,让问题尽早暴露,便于定位解决,而性能测试着重发现程序设计方面的问题,使程序在高并发时能保持稳定。
1.2 单元测试编写步骤
- 创建测试文件:测试文件以
*test.go
结尾,例如math_test.go
。 - 编写测试函数:测试函数以
Test
开头,接收*testing.T
参数。
go
func TestAdd(t *testing.T){
// 测试逻辑
}
3. 表格驱动测试:利用结构体切片定义多个测试用例,循环遍历执行。
go
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 %d\n",v.a,v.b,sum,v.want)
}
}
}
4. 子测试:使用t.Run()
对测试用例进行分组,提升可读性与选择性运行。
go
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 %d\n",v.a,v.b,sum,v.want)
}
}
}
5. 错误测试:验证函数是否返回预期错误。
go
func TestDivide(t *testing.T){
_,err:=Divide(6,0)
if err ==nil{
t.Fatal("预期错误,并未返回")
}
if err.Error()!="除零错误"{
t.Error("错误消息不符:%s",err)
}
}
6. 测试覆盖率:生成并查看覆盖率报告。
bash
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
7. 初始化与清理:使用TestMain
进行全局设置。
go
func TestMain(m *testing.M){
setup()
code:=m.Run()
teardown()
os.Exit(code)
}
8. 使用t.Cleanup:注册清理函数。
go
func TestDB(t *testing.T){
db:=setupDB()
t.Cleanup(func(){
teardownDB(db)
})
// 测试逻辑
}
9. 模拟依赖:通过接口实现Mock()。
“`go
type Storage interface{
Get(id int) string
}
type MockStorage struct{}
func (m*MockStorage)Get(id int)string{
return “mock”
}
func TestService(ttesting.T){
s:=&Service{storage:&MockStorage{}}
// 测试逻辑
}
“`
10. 跳过测试*:
```go
func TestNetwork(t *testing.T){
if testing.Short(){
t.Skip("短模式下跳过")
}
// 网络相关测试
}
```
-
并行测试:使用
t.Parallel()
加速测试。go
func TestParallel(t *testing.T){
t.Parallel()
// 并发安全测试逻辑
}
1.3 总结
- 测试用例文件名必须以
test.go
结尾,比如cal_test.go
,cal
非固定。 - 测试用例函数必须以
Test
开头,通常为Test+被测试函数名
,如TestAddUpper
,形参类型必须是*testing.T
。 - 一个测试用例文件中可包含多个测试用例函数,如
TestAddUpper
。 - 运行测试用例指令:
cmd>go test
(运行正确无日志,错误时输出日志)cmd>gotest-v
(运行正确或错误都输出日志)- 出现错误时,可用
t.Fatalf
格式化输出错误信息并退出程序。 t.Logf
方法可输出相应日志。- 测试用例函数未在
main
函数中却能执行,这是测试用例的便捷之处。 PASS
表示测试用例运行成功,FAIL
表示测试用例运行失败。
1.4 单元测试综合案例
被测代码文件: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
2. goroutine
2.1 进程与线程说明
- 进程:程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
- 线程:进程的一个执行实例,是程序执行的最小单元,是比进程更小的能独立运行的基本单位。
- 一个进程可创建和销毁多个线程,同一个进程中的多个线程可并发执行。
- 一个程序至少有一个进程,一个进程至少有一个线程。
2.2 进程线程关系示意图

2.3 并发与并行
- 多线程程序在单核上运行是并发;在多核上运行是并行。
- 并发:在一个CPU上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但微观上某一时间点只有一个线程在执行。
- 并行:在多个CPU上(比如有10个CPU),比如有10个线程,每个线程执行10毫秒(各自在不同CPU上执行),从人的角度看,这10个线程都在运行,微观上某一时间点同时有10个线程在执行。


2.4 Go协程与Go主线程
Go主线程(也可称为线程或进程):一个Go线程上可起多个协程,协程可理解为轻量级的线程(编译器做优化)。
Go协程特点:
– 有独立栈空间;
– 共享程序堆空间;
– 调度由用户控制;
– 是轻量级线程。
2.5 goroutine使用案例
使用 go 关键字启动协程
package main
import(
"fmt"
"time"
)
func test(){
for i:=0;i<10;i++{
fmt.Printf("test()~%v\n",i)
time.Sleep(1000*time.Millisecond)//休眠一秒
}
}
func main(){
//启动协程
go test()
for i:=0;i<5;i++{
fmt.Printf("main()~%v\n",i)
time.Sleep(1000*time.Millisecond)//休眠一秒
}
}
说明:
– 主线程是物理线程,直接作用在CPU上,重量级,耗费CPU资源;
– 协程从主线程开启,是轻量级线程,逻辑态,对资源消耗相对小;
– Golang的协程机制重要,可轻松开启上万个协程,其他编程语言并发机制多基于线程,开启过多线程资源耗费大,突显Golang并发优势;
– 当主线程终止时,其他协程也将终止。
2.6 MPG基本介绍
- M:代表内核线程,也叫工作线程,goroutine跑在M之上。
- P:代表Processor(处理器),主要用于执行goroutine,维护可运行和自由的goroutine队列。
- G:代表goroutine实际的数据结构,维护goroutine的栈、程序计数器及所在M等信息。
- Scheduler:代表调度器,维护空闲的M队列、空闲的P队列、可运行的G队列、自由的G队列及调度器状态信息等。

2.7 设置Go运行cpu的个数
所用到的方法:

案例:
package main
import (
"fmt"
"runtime"
)
func main(){
//获取当前(逻辑)cpu的数量
num:=runtime.NumCPU()
//设置num-1的cpu运行go程序
runtime.GOMAXPROCS(num)
fmt.Println(num)
}
3. channel(管道)
3.1 全局变量+互斥锁解决资源竞争

案例: 启动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]=%d\n",i,v)
}
//解锁
lock.Unlock()
}
3.2 channel的介绍
- channel本质是数据结构-队列,数据先进先出(FIFO)。
- 线程安全,多goroutine访问时无需加锁。
- 类型安全,只能发送和接收指定类型数据,如string类型channel只能存string数据。
- 无缓冲channel同步,发送和接收操作会阻塞直到另一端准备好。
- 缓冲channel允许在无接收者时发送有限数量数据。
- 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 //也可以不接受数据,将数据推出
3.3 channel的使用案例
(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,容量为%v\n",intChan,&intChan,len(intChan),cap(intChan))
//从channel中取数据
var num int
num=<-intChan
fmt.Println("num=",num)
fmt.Printf("intChan 的长度为%v,容量为%v\n",len(intChan),cap(intChan))
//注意:在没有使用协程的情况下,如果我们的管道数据已经全部取出,继续取数据就会报deadlock
num2:=<-intChan
<-intChan //数据出channel 也可以不接受,相当于丢弃
num3:=<-intChan
fmt.Printf("num2=%d , num3=%d\n",num2,num3)
//num2=2 , num3=4
//数据出channel时先进先出的
}
(2)struct管道和map管道:
“`go
package main
import (
“fmt”
)
type Cat struct{
Name string
Age int
}
func main(){