单元测试
基本介绍
Go中自带轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试。testing框架与其他测试框架类似,可以基于这个框架写相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以实现:
- 确保每个函数是可运行的,且运行结果正确;
- 确保写出来的代码性能;
- 单元测试能及时的发现程序设计或实现的逻辑错误;
- 性能测试的重点在于发现程序设计上的一些问题,使程序能接受高并发的情况;
快速入门总结
快速入门案例代码见坚果云/home/daihaorui/桌面/MyNutcloud/wlhiot_manage/goproject/src/go_code/project/unit_test/testcase01/main
- 测试用例文件名以_test.go结尾;
- 测试用例函数以Test开头;
Test[函数名](t *testing.T)
的形参类型固定为*testing.T;- 使用go test执行单元测试时会将本目录下的所有测试用例文件均执行,-v参数详细输出;
- 出现错误时,可以使用
t.Fatalf
来格式化输出错误信息,并退出程序; t.Logf()
方法可以输出相应日志;- 测试单个文件
go test cal_test.go cal.go
& 测试单个方法go test -test.run TestAddUpper
综合案例
需求:编写一个结构体,拥有字段,并绑定两个方法(序列化及反序列化结构体),编写测试用例进行测试
我方代码
// main.go
package main
import (
"fmt"
"encoding/json"
)
type Monster struct{
Name string `json:"name"`
Age int `json:"age"`
Skill string `json:"skill"`
}
func (a Monster) Store(monster *Monster) []byte {
data, err := json.Marshal(&monster)
if err != nil {
fmt.Printf("序列化失败 err=%v",err)
}
return data
}
func (a Monster) ReStore(monster []byte) Monster {
var monster1 Monster err := json.Unmarshal(monster,&monster1)
if err != nil {
fmt.Printf("反序列化失败 err=%v",err)
}
return monster1
}
func main() {
monster := Monster{
Name:"牛魔王",
Age:14,
Skill: "打豆豆",
}
json1 := monster.Store(&monster)
fmt.Println(string(json1))
struct1 := monster.ReStore(json1)
fmt.Println(struct1)
}
// main_test.go
package main
import (
"testing"
)
func TestStore(t *testing.T) {
monster := Monster{
Name : "牛魔王",
Age: 14,
Skill: "打豆2豆",
}
res := monster.Store(&monster)
str := "{\"name\":\"牛魔王\",\"age\":14,\"skill\":\"打豆豆\"}"
if string(res) != str {
t.Fatalf("测试失败,预期=%v,实际=%v",str,string(res))
}else {
t.Logf("测试通过")
}
}
教程代码
// main.go
package monster
import (
"encoding/json"
"io/ioutil"
"fmt"
)
type Monster struct {
Name string
Age int
Skill
string
} //给Monster绑定方法Store, 可以将一个Monster变量(对象),序列化后保存到文件中
func (this *Monster) Store() bool {
//先序列化
data, err := json.Marshal(this)
if err != nil {
fmt.Println("marshal err =", err)
return false
}
//保存到文件
filePath := "d:/monster.ser"
err = ioutil.WriteFile(filePath, data, 0666)
if err != nil {
fmt.Println("write file err =", err)
return false
}
return true
}
//给Monster绑定方法ReStore, 可以将一个序列化的Monster,从文件中读取,
//并反序列化为Monster对象,检查反序列化,名字正确
func (this *Monster) ReStore() bool {
//1. 先从文件中,读取序列化的字符串
filePath := "d:/monster.ser"
data, err := ioutil.ReadFile(filePath)
if err != nil {
fmt.Println("ReadFile err =", err)
return false
}
//2.使用读取到data []byte ,对反序列化
err = json.Unmarshal(data, this)
if err != nil {
fmt.Println("UnMarshal err =", err)
return false
}
return true
}
// main_test.go
package monster
import "testing"
//测试用例,测试 Store 方法
func TestStore(t *testing.T) {
//先创建一个Monster 实例
monster := &Monster{
Name : "红孩儿",
Age :10, Skill : "吐火.",
}
res := monster.Store()
if !res {
t.Fatalf("monster.Store() 错误,希望为=%v 实际为=%v", true, res)
} t.Logf("monster.Store() 测试成功!")
}
func TestReStore(t *testing.T) {
//测试数据是很多,测试很多次,才确定函数,模块..
//先创建一个 Monster 实例 , 不需要指定字段的值
var monster = &Monster{}
res := monster.ReStore()
if !res {
t.Fatalf("monster.ReStore() 错误,希望为=%v 实际为=%v", true, res)
}
//进一步判断
if monster.Name != "红孩儿" {
t.Fatalf("monster.ReStore() 错误,希望为=%v 实际为=%v", "红孩儿", monster.Name)
}
t.Logf("monster.ReStore() 测试成功!")
}
goroutine(协程)
基本介绍
- 并发:多线程程序在单核上运行(单核运行多个线程)
- 并行:多线程程序在多核上运行
Go协程和Go主线程
Go主线程(也可称之为线程或理解为进程),一个Go线程上,有多个协程,可以理解为协程是轻量级的线程(编译器做优化)
Go协程的特点:
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
快速入门总结
- 主线程是一个物理线程,直接作用在CPU上。是重量级的,非常耗费CPU资源
- 协程是从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
- Golang的协程机制是重要的特点,可以轻松开启上万个协程。其他编程语言的并发机制一般是基于线程的,可以过多的线程会导致资源消耗大,这就是golang在并发上的优势
goroutine调度模型
MPG模式
M:操作系统的主线程(物理线程)
P:协程执行需要的context
G:协程
channel(管道)
不同gorouteine之间有两种方式来通讯。初阶使用全局变量的互斥锁,高阶使用管道channel来解决
互斥锁
多个goroutine协程对全局变量进行写操作的时候,会出现资源争夺的问题,可能会报concurrent map writes错误,所以可以加入互斥锁使其单次写入。
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 += n
}
lock.Lock() // 锁
myMap[n] = res // 写数据
lock.Unlock() // 解锁
}
func main() {
for i := 1; i <= 200; i++ {
go test(i)
}
time.Sleep(time.Second * 3)
for i, v := range myMap {
// 可能出现互斥的情况,可在该行前面加锁后面解锁
fmt.Printf("map[%d]=%d\n", i, v)
}
fmt.Println(len(myMap))
}
channel
基本介绍
- channel本质是一个数据结构-队列
- 数据是先进先出(first in first out)
- 线程安全,多gorouteine访问时,不需要加锁
- channel是有类型的,一个string的channel只能存放string类型数据
基本使用
channel是引用类型,必须初始化(make)后才能写入数据,管道是有类型的,例如intChan只能写入整数int
var [变量名] chan [数据类型] // 声明语法
package main
import "fmt"
func main() {
// 定义管道
var intChan chan int
intChan = make(chan int, 3)
// intChan的值是地址(引用类型)
fmt.Println(&intChan, intChan)
// 向管道写入数据
intChan <- 10
num := 211
intChan <- num
// 查看管道的长度和容量
fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan))
// 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println(num2)
}
channel的关闭
使用内置函数close()
可以关闭channel,当channel关闭后,就不能再向channel写数据,但是仍然可以从该channel读取数据
channel的遍历
channel支持for-range
方式进行遍历,但是需要注意以下细节:
- 在遍历时,如果channel没有关闭,则会出现deadlock错误
- 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
阻塞
如果只是向管道中写入数据,而不去读取,就会出现deadlock死锁(前提是管道容器不足以支撑写入的数据量)。
读写频率无所谓的
waitGropu学习
案例
分析
代码
package main
import ( "fmt" )
func putNum(intChan chan int) {
for i := 0; i < 80000; i++ {
intChan <- i
}
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
num, ok := <-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
}
}
exitChan <- true
}
func main() {
goroutine := 8
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
exitChan := make(chan bool, goroutine)
go putNum(intChan)
for i := 0; i < goroutine; i++ {
go primeNum(intChan, primeChan, exitChan)
}
go func() {
// 这块也采用goroutine来
for i := 0; i < goroutine; i++ {
<-exitChan
}
close(primeChan)
}()
for i := range primeChan {
fmt.Println("素数有", i)
}
}
注意事项及使用细节
- 可根据实际情况将属性声明为"可读写"、"只读"、"只写"
- 实际应用场景:用于设定函数的参数的管道,及设置推送函数的管道为只写,接受函数的管道为只读,可以很好的杜绝误操作
var chan1 chan int // 可读写
var chan2 chan <- int // 只写
var chan3 <- chan int // 只度
- 使用select可以解决从管道取数据的阻塞问题(就是不关闭管道也能去读)
package main
import (
"fmt"
"time"
)
func main() {
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}
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)
case v := <-stringChan :
fmt.Printf("从stringChan读取的数据%s\n", v)
default :
fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
return // 跳出main()方法
// break label
}
}
}
- goroutine中使用recover,解决协程中出现panic导致的程序崩溃问题
func test() {
defer func () {
if err := recover();err != nil {
fmt.Println("test()发生错误",err)
}
}()
// var myMap map[int]string // 错误函数,可解开注释来测试
myMap := make(map[int]string, 10)
myMap[1] = "golang"
}
反射(reflect)
使用场景包含如下:
- JSON序列化及反序列化
- 编写函数的适配器,桥连接
基本介绍
- reflect可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind)
- 如果是struct变量,还可以获取到struct本身的信息(包括struct的字段、方法)
- 通过reflect,可以修改变量的值,可以调用关联的方法
- 使用reflect,需要import ("reflect")
- 变量、
interface{}
和reflect.Value
是可以相互转换的
package main
import (
"fmt"
"reflect"
)
type Stu struct {
Name string
}
func test(b interface{}) {
// 1. 将interface{}转成reflect.Value
rVal := reflect.ValueOf(b) // rVal的类型其实是reflect.Value
// 2. 将reflect.Value转成interface{}
iVal := rVal.Interface()
// 3. 将interface{}转成原来的变量类型,使用类型断言
v := iVal.(Stu)
fmt.Println(v.Name) // 获取v Stu的Name值
}
func main() {
student := Stu{
Name: "daihaorui",
}
test(student)
}
注意事项和细节说明
-
reflect.Value.Kind
,获取变量的类型,返回的是一个常量 -
Type和Kind不一定相同
-
var num int = 10
时,num的Type为int
,Kind也为int
-
var stu Student
时,stu的Type为[包名].Student
,Kind为struct
-
通过反射可以让变量在interface{}和reflect.Value之间互相转换
-
使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配。例如x为int,那么就应该用reflect.Value(x).Int()
1 . 如果是结构体,则需要使用断言来获取数据类型,因为reflect.ValueOf()
并没有提供对struct的类型转换
- 通过反射来修改变量,,注意当使用SetXxx方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量的值,同时需要使用到
reflect.Value.Elem()
方法
func reflect01(b interface{}) {
rVal := reflect.ValueOf(b)
rVal.Elem().SetInt(20)
} func main() {
var num int = 10
reflect01(&num)
fmt.Println(num) // 20
}
最佳实践
- 使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值
package main
import(
"fmt"
"reflect"
)
//定义了一个Monster结构体
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32 `json:"成绩"`
Sex string
}
//方法,返回两个数的和
func (s Monster) GetSum(n1, n2 int) int { return n1 + n2 }
//方法, 接收四个值,给s赋值
func (s Monster) Set(name string, age int, score float32, sex string) {
s.Name = name
s.Age = age
s.Score = score
s.Sex = sex
}
//方法,显示s的值
func (s Monster) Print() {
fmt.Println("---start~----")
fmt.Println(s)
fmt.Println("---end~----")
}
func TestStruct(a interface{}) {
//获取reflect.Type 类型
typ := reflect.TypeOf(a)
//获取reflect.Value 类型
val := reflect.ValueOf(a)
//获取到a对应的类别
kd := val.Kind()
//如果传入的不是struct,就退出
if kd != reflect.Struct {
fmt.Println("expect struct")
return
}
//获取到该结构体有几个字段
num := val.NumField()
fmt.Printf("struct has %d fields\n", num) //4
//变量结构体的所有字段
for i := 0; i < num; i++ {
fmt.Printf("Field %d: 值为=%v\n", i, val.Field(i))
//获取到struct标签, 注意需要通过reflect.Type来获取tag标签的值
tagVal := typ.Field(i).Tag.Get("json")
//如果该字段于tag标签就显示,否则就不显示
if tagVal != "" {
fmt.Printf("Field %d: tag为=%v\n", i, tagVal)
}
}
//获取到该结构体有多少个方法
numOfMethod := val.NumMethod()
fmt.Printf("struct has %d methods\n", numOfMethod)
//var params []reflect.Value
//方法的排序默认是按照 函数名的排序(ASCII码)
val.Method(1).Call(nil) //获取到第二个方法。调用它
//调用结构体的第1个方法Method(0)
var params []reflect.Value //声明了 []reflect.Value
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(40))
res := val.Method(0).Call(params) //传入的参数是 []reflect.Value, 返回[]reflect.Value
fmt.Println("res=", res[0].Int()) //返回结果, 返回的结果是 []reflect.Value*/
}
func main() {
//创建了一个Monster实例
var a Monster = Monster{
Name: "黄鼠狼精",
Age: 400,
Score: 30.8,
} //将Monster实例传递给TestStruct函数 TestStruct(a)
}
- 使用反射的方式来获取结构体的tag标签,遍历字段的值,修改字段值,调用结构体方法(通过传递地址的方式完成)(学习)
- 定义了两个函数test1和test2,定义一个适配器函数用作统一处理接口(了解)
- 使用反射操作任何结构体类型(了解)
- 使用反射创建并操作结构体(了解)
package main
import (
"fmt"
"reflect"
)
type Cal struct {
Num1 int
Num2 int
}
func (c Cal) GetSub(name string) {
fmt.Printf("%v完成了减法运行,%d - %d = %d", name, c.Num1, c.Num2, c.Num1-c.Num2)
}
func reflect01(c interface{}) {
rVal := reflect.ValueOf(c)
iVal := rVal.Interface()
cal := iVal.(Cal)
fmt.Println(cal.Num1, cal.Num2)
var params []reflect.Value
params = append(params, reflect.ValueOf("tom"))
rVal.Method(0).Call(params)
}
func main() {
cal := Cal{
Num1: 8,
Num2: 3,
}
reflect01(cal)
}