Golang_06_并发及反射


目录:

单元测试

基本介绍

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

  1. 确保每个函数是可运行的,且运行结果正确;
  2. 确保写出来的代码性能;
  3. 单元测试能及时的发现程序设计或实现的逻辑错误;
  4. 性能测试的重点在于发现程序设计上的一些问题,使程序能接受高并发的情况;

快速入门总结

快速入门案例代码见坚果云/home/daihaorui/桌面/MyNutcloud/wlhiot_manage/goproject/src/go_code/project/unit_test/testcase01/main

  1. 测试用例文件名以_test.go结尾;
  2. 测试用例函数以Test开头;
  3. Test[函数名](t *testing.T)的形参类型固定为*testing.T;
  4. 使用go test执行单元测试时会将本目录下的所有测试用例文件均执行,-v参数详细输出;
  5. 出现错误时,可以使用t.Fatalf来格式化输出错误信息,并退出程序;
  6. t.Logf()方法可以输出相应日志;
  7. 测试单个文件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(协程)

基本介绍

  1. 并发:多线程程序在单核上运行(单核运行多个线程)
  2. 并行:多线程程序在多核上运行

Go协程和Go主线程

Go主线程(也可称之为线程或理解为进程),一个Go线程上,有多个协程,可以理解为协程是轻量级的线程(编译器做优化)

Go协程的特点:

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

快速入门总结

  1. 主线程是一个物理线程,直接作用在CPU上。是重量级的,非常耗费CPU资源
  2. 协程是从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
  3. 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

基本介绍

  1. channel本质是一个数据结构-队列
  2. 数据是先进先出(first in first out)
  3. 线程安全,多gorouteine访问时,不需要加锁
  4. 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方式进行遍历,但是需要注意以下细节:

  1. 在遍历时,如果channel没有关闭,则会出现deadlock错误
  2. 在遍历时,如果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)
    }
}

注意事项及使用细节

  1. 可根据实际情况将属性声明为"可读写"、"只读"、"只写"
  2. 实际应用场景:用于设定函数的参数的管道,及设置推送函数的管道为只写,接受函数的管道为只读,可以很好的杜绝误操作
var chan1 chan int  // 可读写
var chan2 chan <- int  // 只写
var chan3 <- chan int // 只度
  1. 使用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
        }
    }
}
  1. 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)

使用场景包含如下:

  1. JSON序列化及反序列化
  2. 编写函数的适配器,桥连接

基本介绍

  1. reflect可以在运行时动态获取变量的各种信息,比如变量的类型(type)类别(kind)
  2. 如果是struct变量,还可以获取到struct本身的信息(包括struct的字段、方法)
  3. 通过reflect,可以修改变量的值,可以调用关联的方法
  4. 使用reflect,需要import ("reflect")
  5. 变量、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)
}

注意事项和细节说明

  1. reflect.Value.Kind,获取变量的类型,返回的是一个常量

  2. Type和Kind不一定相同

  3. var num int = 10时,num的Type为int,Kind也为int

  4. var stu Student时,stu的Type为[包名].Student,Kind为struct

  5. 通过反射可以让变量interface{}reflect.Value之间互相转换

  6. 使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配。例如x为int,那么就应该用reflect.Value(x).Int()

1 . 如果是结构体,则需要使用断言来获取数据类型,因为reflect.ValueOf()并没有提供对struct的类型转换

  1. 通过反射来修改变量,,注意当使用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
}

最佳实践

  1. 使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值
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)
}
  1. 使用反射的方式来获取结构体的tag标签,遍历字段的值,修改字段值,调用结构体方法(通过传递地址的方式完成)(学习)
  2. 定义了两个函数test1和test2,定义一个适配器函数用作统一处理接口(了解)
  3. 使用反射操作任何结构体类型(了解)
  4. 使用反射创建并操作结构体(了解)

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)
}