Golang第十篇:单元测试、go协程及通道剖析

2天前发布 gsjqwyl
2 0 0

Golang第十篇:单元测试、go协程与通道解析

1. 单元测试

1.1 基础阐述

Go语言自带轻量级测试框架testing以及go test命令来开展单元测试与性能测试。该测试框架和其他语言的测试框架类似,可基于它编写针对相应函数的测试用例,也能编写压力测试用例。通过单元测试,能达成以下目标:
1. 确保每个函数可正常运行且结果正确;
2. 保证所编写代码性能良好;
3. 及时发现程序设计或实现中的逻辑错误,让问题尽早暴露,便于定位解决,而性能测试着重发现程序设计方面的问题,使程序在高并发时能保持稳定。

1.2 单元测试编写步骤

  1. 创建测试文件:测试文件以*test.go结尾,例如math_test.go
  2. 编写测试函数:测试函数以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("短模式下跳过")
    }
    // 网络相关测试
}
```
  1. 并行测试:使用t.Parallel()加速测试。

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

1.3 总结

  1. 测试用例文件名必须以test.go结尾,比如cal_test.gocal非固定。
  2. 测试用例函数必须以Test开头,通常为Test+被测试函数名,如TestAddUpper,形参类型必须是*testing.T
  3. 一个测试用例文件中可包含多个测试用例函数,如TestAddUpper
  4. 运行测试用例指令:
  5. cmd>go test(运行正确无日志,错误时输出日志)
  6. cmd>gotest-v(运行正确或错误都输出日志)
  7. 出现错误时,可用t.Fatalf格式化输出错误信息并退出程序。
  8. t.Logf方法可输出相应日志。
  9. 测试用例函数未在main函数中却能执行,这是测试用例的便捷之处。
  10. 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 并发与并行

  1. 多线程程序在单核上运行是并发;在多核上运行是并行。
  2. 并发:在一个CPU上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但微观上某一时间点只有一个线程在执行。
  3. 并行:在多个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基本介绍

  1. M:代表内核线程,也叫工作线程,goroutine跑在M之上。
  2. P:代表Processor(处理器),主要用于执行goroutine,维护可运行和自由的goroutine队列。
  3. G:代表goroutine实际的数据结构,维护goroutine的栈、程序计数器及所在M等信息。
  4. Scheduler:代表调度器,维护空闲的M队列、空闲的P队列、可运行的G队列、自由的G队列及调度器状态信息等。
MPG示意图

2.7 设置Go运行cpu的个数

所用到的方法:

设置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的介绍

  1. channel本质是数据结构-队列,数据先进先出(FIFO)。
  2. 线程安全,多goroutine访问时无需加锁。
  3. 类型安全,只能发送和接收指定类型数据,如string类型channel只能存string数据。
  4. 无缓冲channel同步,发送和接收操作会阻塞直到另一端准备好。
  5. 缓冲channel允许在无接收者时发送有限数量数据。
  6. 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(){

© 版权声明

相关文章

暂无评论

暂无评论...