Go语言学习篇06

Go语言学习篇06

单元测试

传统测试方法

问题:

​ 有一个函数,怎样确认它运行的结果正确?

传统方法解决方案

​ 在main函数中,调用addUpper函数,看看实际输出结果是否和预期的结果一致,

如果一致,则说明函数正确,否则函数有错误,然后修改错误。

代码

package main

import "fmt"

// A function under test
func addUpper(n int) int {
	res := 0
	for i := 0; i <= n; i++ {
		res +=i
	}
	return res
}

func main() {
	var n int = 10
	sum := addUpper(n)
	if sum != 55 {
		fmt.Printf("addUpper error, return value = %v expected value = %v\n", sum, 55)
	} else {
		fmt.Printf("addUpper correct, return value = %v expected value = %v\n", sum, 55)
	}
}

传统方法的缺陷

1)不方便测试:我们需要在main中调用,如果项目正在运行,就需要停止程序

2)不利于管理:我们测试多个函数、模块时,都要写在main函数中,不利于管理和清晰我们的思路

3)引出单元测试。testing测试框架

testing测试框架

基本介绍

​ Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试性能测试

  1. 可以基于这个框架写针对函数的测试用例

  2. 可以基于框架写相应的压力测试用例

可以解决的问题

1)确保每个函数是可以运行,并且运行结果是正确的

2)确保写出来的代码性能是好的

3)单元测试及时发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题定位解决;

性能测试的重点在于发现程序设计上的一些问题让程序能够在高并发的情况下仍能保持稳定

testing的入门使用

import "testing"

testing 提供对 Go 包的自动化测试的支持。通过 go test 命令,能够自动执行如下形式的任何函数:

func TestXxx(*testing.T)

其中 Xxx 可以是任何字母数字字符串(但第一个字母不能是 [a-z],必须大写),用于识别测试例程(程序)。

在这些函数中,使用 Error, Fail 或相关方法来发出失败信号。

Go语言学习篇06

错误案例

D:\Work\Goland\Go\src\unitTest-chapter\unit-case\main>go test -v
=== RUN   TestAddUpper
    main_test.go:15: AddUpper(10)执行错误,期望值是55,实际值是45
--- FAIL: TestAddUpper (0.00s)
FAIL
exit status 1
FAIL    unitTest-chapter/unit-case/main 0.244s

正确案例

D:\Work\Goland\Go\src\unitTest-chapter\unit-case\main>go test -v
=== RUN   TestAddUpper
    main_test.go:18: AddUpper(10)执行正确,期望值是55,实际值是55
--- PASS: TestAddUpper (0.00s)
PASS
ok      unitTest-chapter/unit-case/main 0.250s

Testing快速入门总结

1)测试用例文件必须以_test.go结尾

2)测试用例必须以Test+[A-Z]开头

3)TestAxxx( t *testing.T )的形参类型必须是*testing.T

4)一个测试用例文件中,可以有多个测试用例函数

5)运行测试用例指令

​ (1) cmd>go test [如果运行正确,无日志,错误时,会输出日志]

​ (2) cmd>go test -v [如果运行正确或错误,都输出日志]

​ (3) cmd>go test -v main_test.go main.go,执行指定的单个文件

​ (4) cmd>go test -v -test.run TestAddUpper,测试单个方法

6)当出现错误时,可以使用t.Fatalf格式化输出错误信息,并退出程序

7)t.Logf,格式化输出相应日志

8)测试用例函数,并没有放在main函数中,也执行了,这就是测试用例的方便之处

9)PASS表示测试用例运行成功,FALL表示测试用例运行失败

单元测试综合案例

案例要求

1)编写一个Monster结构体,字段Name,Age,Skill

2)给Monster绑定一个方法Store,可以将变量序列化后保存到文件中

3)给Monster绑定一个Restore方法,可以将序列化的Monster从文件中读取,并反序列化成Monster对象

4)编程测试调用文件 store_test.go,编写测试调用例TestStore和TestRestore进行测试

案例代码

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
)

var (
	filePath = "C:/Users/海角天涯S/Desktop/monster.txt"
)

type Monster struct {
	Name string
	Age int
	Skill string
}

//工厂模式
func NewMonster(name string, age int, skill string) *Monster {
	return &Monster{
		Name:  name,
		Age:   age,
		Skill:	skill,
	}
}

//be to marshal
func (monster *Monster) Store() bool {
	byteSlice, jsonError := json.Marshal(monster)
	if jsonError != nil {
		fmt.Printf("Serialization error,%v\n", jsonError)
		return false
	}

	//将文件保存至文件中
	writeError := ioutil.WriteFile(filePath, byteSlice, 0666)
	if writeError != nil {
		fmt.Printf("Write file error,%v\n", writeError)
		return false
	}
	return true
}

//be to unmarshal
func (monster *Monster) ReStore() bool {
	//读取文件
	data, readError := ioutil.ReadFile(filePath)
	if readError != nil {
		fmt.Printf("Read file error,%v\n", readError)
		return false
	}
	error := json.Unmarshal(data, monster)
	if error !=nil {
		fmt.Printf("Unmarshal error,%v\n", error)
		return false
	}
	fmt.Println(*monster)
	return true
}

测试代码

package main

import (
	"testing"
)

var (
	monster = NewMonster("carter", 20, "上网")
)

func TestStore(t *testing.T) {
	flag := monster.Store()
	if !flag {
		t.Fatal("情报有误...")
	}
}

func TestReStore(t *testing.T) {
	flag := monster.ReStore()
	if !flag {
		t.Fatal("情报有误...")
	}
}

Goroutine (协程)

Goroutine基本介绍

进程和线程说明

  • 进程

    • 是程序在操作系统中的一次执行过程
    • 前台进程
    • 后台进程
  • 线程

    • 线程是进程的一个执行实例
    • 程序执行的最小单位
    • 百度网盘同时下载多个资源
    • 同一个百度网盘(进程)中的多个线程(网盘资源)可以并发执行(同时下载)

    一个程序至少一个进程,一个进程至少一个线程

打开一个进程百度网盘

多线程下载多任务下载

Go语言学习篇06

并发和并行说明

1)多线程程序在单核上运行,就是并发

2)多线程程序在多核上运行,就是并行

Go语言学习篇06

Go协程和Go主线程

单核线程—>多核线程(占CPU大)—>协成(占CPU小)

  • Go协程是轻量级的线程

  • Go主线程含多个协程万级别

  • 协程的特点

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

Goroutine快速入门

案例说明

请编写一个程序,完成如下功能:

1)在主线程中开启一个Goroutine,该协程每隔1秒输出"hello world"

2)在主线程中每隔1秒输出"hello golang",输出10次后退出程序

3)要求主线程协程goroutine同时执行

Go语言学习篇06

Go语言学习篇06

Goroutine快速入门小结

1)主线程是一个物理线程,直接作用在Cpu上。是一个重量级的,非常消耗CPU资源。

2)协程是从主线程开启的,是轻量级的线程,是逻辑态的。对资源消耗较小

3)Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言并发机制一般基于线程,开启过多的线程,资源耗费大,这就是Golang在并发上的优势

Goroutine的调度模型

MPG模式基本介绍

1)M:操作系统的主线程(物理线程)

2)P:协程执行需要的上下文环境

3)G:协程

MPG模式运行状态

Go语言学习篇06

Go语言学习篇06

设置Golang运行的CPU数量

package main

import (
	"fmt"
	"runtime"
)

func main() {
	cpuNum := runtime.NumCPU()
	fmt.Println("电脑CPU数目:", cpuNum)

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

总结

1)go1.8后,默认让程序运行在多个核上,可以不用设置了

2)go1.8前,需要设置,可以更高效的利用CPU

求素数问题

需求:

​ 统计1-200,000,000,000的数字中,哪些是素数?

分析思路:

​ 1)传统方法,使用for循环对每个数进行判定 【不能发挥多核的优势】

​ 2)使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成

Goroutine解决问题

代码

package main

import (
	"fmt"
	"math"
	"runtime"
	"time"
)

// 放入1~n个数
func DeterminePrimeNumber (intChan chan int, n int) {
	for i := 2; i <= n; i++ {
		var flag bool = true

		for j := 2; float64(j) <= math.Sqrt(float64(i)); j += 2 {
			// 非素数
			if i%j == 0 {
				flag = false
				break
			}
		}
		if flag {
			intChan <- i
		}
	}
	close(intChan)
}

func ResultNum(intChan chan int, resultChan chan int, exitChan chan bool)  {
	for {
		// 读取延迟 10 毫秒
		time.Sleep(time.Millisecond * 10)
		value, ok := <- intChan
		if !ok {
			break
		}
		resultChan <- value
	}
	fmt.Println("有一个ResultNum协程因为取不到数据,退出!")
	// 这里resultChan还不能关闭,因为别人可能在处理
	exitChan <- true
}

func RangeChan(resultChan chan int) {
	for value := range resultChan {
		fmt.Println(value)
	}
}

func main() {
	var endNum int = 100
	var cpuNum int = runtime.NumCPU()

	var intChan chan int = make(chan int, endNum)
	var resultChan chan int = make(chan int, endNum)
	var exitChan chan bool = make(chan bool, cpuNum)

	go DeterminePrimeNumber(intChan, endNum)
	for i := 0; i < cpuNum; i++ {
		go ResultNum(intChan, resultChan, exitChan)
	}

	go func() {
		// exitChan有4个true,退出
		for i := 0; i < cpuNum; i++ {
			<- exitChan
		}

		// 这时候即可放心关闭 resultChan
		close(resultChan)
	}()

	// 遍历素数
	RangeChan(resultChan)

	fmt.Println("main线程退出...")
}

结果

2
3
5
7
....
89
91
93
95
97
99
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!// 8个
main线程退出...

channel(管道)

基本介绍

1)channel本质就是一个数据结构-队列

2)数据先进先出【FIFO】

3)线程安全,多goroutine访问时,不需要加锁,就是说channel本身是线程安全的

4)channel是有类型的,一个string的channel只能存放string数据类型

Go语言学习篇06

资源竞争问题

需求:

​ 现在计算1-200的各个数的阶乘,并且把各个数的阶乘放入map中。最后显示出来。需要使用goroutine完成

分析思路

1)使用goroutine 来完成,效率高,但是会出现并发、并行问题

2)这里就提出了不同的goroutine如何通信问题,使用

cmd> go build -race main.go,
cmd> main.exe
Found 3 data race(s):发现3个资源有竞争

Go语言学习篇06

代码实现

1)使用goroutine来完成

2)在某个程序时,如何知道是否存在资源竞争问题

package main

import (
	"fmt"
)

var (
	myMap = make(map[int]int)
)

//1、计算各个数的阶乘,并放入到map中
//2、我们启动多个协程,统计的将结果放入map中
//3、map是全局的

func factorial(n int) {
	result := 1
	for i := 1; i <= n; i++ {
		result *= i
	}
	myMap[n] = result
}

func main() {
	for i := 1; i <= 200; i++ {
		go factorial(i) //fatal error: concurrent map writes
	}
	//输出结果nil,main线程先结束....
	for index, value := range myMap {
		fmt.Printf("map[%v]=%d\n", index, value)
	}
}

问题

  • 因为没有全局变量加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
  • 解决方案加入互斥锁
  • 我们的阶乘很大,var result uint64接收

方案1:全局变量加锁同步

资源竞争关系解决方案?

Go语言学习篇06

  • 判断是lock状态,还是unlock状态
    • lock状态不准进,请排队吧
    • unlock允许进入

代码

package main

import (
	"fmt"
	"sort"
	"sync"
	"time"
)

var (
	myMap = make(map[int]uint64)
	//Define a global mutex
	lock sync.Mutex
)

func factorial(n int) {
	var result uint64 = 1
	for i := 1; i <= n; i++ {
		result *= uint64(i)
	}
	//加锁
	lock.Lock()
	myMap[n] = result
	//解锁
	lock.Unlock()
}

func main() {
	for i := 1; i <= 65; i++ {
		go factorial(i) //fatal error: concurrent map writes
	}
    
	//延迟5秒,不延迟就嗝屁
	time.Sleep(5 * time.Second)
	
	lock.Lock()
    //排序
	index := make([]int, 0)
	for key, _ := range myMap {
		index = append(index, key)
	}
	sort.Ints(index)
	//为什么这里需要加互斥锁呢?
	for _, value := range index {
		fmt.Printf("map[%v]=%d\n", value, myMap[value])
	}
	lock.Unlock()
}

Go语言学习篇06

问题

1)主线程并不知道协程运行需要等待的时间

2)不加延迟时间,加不加锁无所谓,协程执行不全

3)不利于局部的读写,局部读写都得加锁

方案2:channel

Go语言学习篇06

Go语言学习篇06

channel(管道)-基本使用

var 变量名 chan 数据类型

举例

var intChan chan int	【存放int数据】
var mapChan chan map[int]string	【存放map[int]string数据】
var perChan01 chan Person
var perChan02 chan *Person

说明

1)channel是引用类型

2)channel必须初始化才能写入数据,即make后才能使用

3)管道是有类型的

Go语言学习篇06

代码

package main

import "fmt"

func main() {
	//1、Define a channel
	var intChan chan int
	intChan = make(chan int, 3)

	//2、print channel value
	//0xc00004a060
	fmt.Printf("intChan空间中的值:%p\n", intChan) //0xc00004a060
	fmt.Printf("intChan本身的地址:%p\n", &intChan) //0xc000006028

	//3、Write data to the channel
	intChan <- 10 //屁股后面追加
	var num01 int = 211
	intChan <- num01
	fmt.Println("读取前:")

	//4、查看管道的长度和容量
	fmt.Println("\tchannel len =", len(intChan))
	//容量最多是3,不可超过本身容量,make的时候定义了协程数量,不可以发生改变
	fmt.Println("\tchannel cap =", cap(intChan))

	//5、从管道中读取一个数据出来,管道长度会减少
	var num02 int
	num02 = <-intChan
	fmt.Println("读取后:")
	fmt.Println("\tchannel len =", len(intChan))
	fmt.Println("\tchannel cap =", cap(intChan))
	fmt.Println("从管道中读取的第一个数据num=", num02)
    intChan<- 100
	<-intChan//读取后再存个0
	intChan<- 0
}

结果

intChan空间中的值:0xc000104080
intChan本身的地址:0xc000006028
读取前:
	channel len = 2
	channel cap = 3
读取后:
	channel len = 1
	channel cap = 3
从管道中读取的第一个数据num= 10

存放任意数据变量

Go语言学习篇06

案例二

Go语言学习篇06

类型断言即可

//需要类型断言
cat := cat02.(Cat)
fmt.Println(cat.Name)

问题

Channel的遍历和关闭

使用内置的close关闭,channel只能读,不能写~

买火车票,大门一关,房子内的可以继续购买,房子外的不可继续购买!

使用for-range遍历

1)如果channel没有关闭,则会出现deadlock的错误

2)如果channel已关闭,则会正常遍历,遍历完后,退出遍历

代码

package main

import "fmt"

func main() {
	// 1、放入100个数据导管道
	intChan := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan <- i*2
	}

	/**
	注意:
		必须关闭管道,否则取到最后会出现死锁问题
	 */
	close(intChan)

	// 2、遍历管道for...range
	// 管道没有下标
	for value := range intChan {
		fmt.Println(value)
	}
}

结果

0
2
4
6
8
10
...
194
196
198

代码

package main

import (
	"fmt"
	"time"
)

func getNum(intChan chan int) {
	time.Sleep(time.Second * 10)
	intChan <- 1
	close(intChan)
}

func main() {
	var intChan chan int = make(chan int, 1)
	go getNum(intChan)

	for {
		fmt.Println("-----1")
		value, ok := <- intChan
		fmt.Println("-----2")
		if ok {
			fmt.Println(value)
		} else {
			break
		}
	}
}

结果

-----1	//此处开始阻塞10秒
-----2	//10秒后读取成功
1
-----1
-----2

Goroutine与Channel的结合使用

解决问题

​ 协程之间的相互通信问题,以及何时结束主线程~

案例

1)开启一个writeData协程,向管道intChan中写入50个整数

2)开启一个readData协程,从管道intChan中读取writeData写入的数据

3)注意:writeData和readData操作同一管道

4)主线程需要等待2个协程都完成工作才能推出(不用lock)

思路

Go语言学习篇06

代码

package main

import (
	"fmt"
	"time"
)

// 同时写
func writeData(intChan chan int) {
	for i := 1; i <= 10; i++ {
		// 放入数据
		intChan <- i
		fmt.Println("writeData 写:", i)
		time.Sleep(time.Second)
	}
	close(intChan)
}

// 同时读
func readData(intChan chan int, exitChan chan bool) {
	for {
		value, ok := <- intChan
		if !ok {
			break
		}
		fmt.Println("readData 读到:", value)
	}

	//任务完成
	exitChan <- true
	close(exitChan)
}

func main() {
	// 1、创建两个Channel
	var intChan chan int = make(chan int, 10)
	var exitChan chan bool = make(chan bool, 1)

	// 2、创建两个Goroutine
	go writeData(intChan)
	go readData(intChan, exitChan)

	// 3、判断是否可以退出
	for {
		_, ok := <- exitChan
		if !ok {
			break
		}
	}
}

结果

writeData 写: 1
readData 读到: 1
writeData 写: 2
readData 读到: 2
readData 读到: 3
writeData 写: 3
writeData 写: 4
readData 读到: 4
writeData 写: 5
readData 读到: 5
writeData 写: 6
readData 读到: 6
writeData 写: 7
readData 读到: 7
writeData 写: 8
readData 读到: 8
writeData 写: 9
readData 读到: 9
writeData 写: 10
readData 读到: 10

问题

写的协程运行的很快,intChan(chan int, 10)—>10容量,写1000

读的协程很慢,虽然会导致写到最大容量时出现阻塞问题,但是不会出现死锁问题!【厕所满员了,你也只能先等着】

如果没有读的协程就会出现容量不足,intChan出现死锁问题

综合练习题:难

Go语言学习篇06

代码

8个协程操作同一个channel,必须八个协程都停止,才能close管道

close管道后才能正常遍历

package main

import (
	"fmt"
)

func GetNum(numChan chan int, endNum int) {
	for i := 1; i <= endNum; i++ {
		numChan <- i
	}
	close(numChan)
}

func Calculation(numChan chan int, resultChan chan uint64, exitChan chan bool, endNum int, i int) {
	for true {
		value, ok := <- numChan
		if !ok {
			break
		} else {
			resultChan <- uint64((value + 1) * value / 2)
		}
	}
	exitChan <- true
	fmt.Printf("第%v个协程工作结束!\n", i)
}

func RangeResult(resultChan chan uint64) {
	for value := range resultChan {
		fmt.Println(value)
	}
}

func main() {
	// 最后n的值, 管道容量
	var endNum int = 1000
	var goroutineNum int = 8
	var numChan chan int = make(chan int, 10)
	var resultChan chan uint64 = make(chan uint64, endNum)
	var exitChan chan bool = make(chan bool, goroutineNum)
	go GetNum(numChan, endNum)
	for i := 1; i <= goroutineNum; i++ {
		go Calculation(numChan, resultChan, exitChan, endNum, i)
	}

	for i := 0; i < goroutineNum; i++ {
		<- exitChan
		fmt.Printf("取出exit中的第%v个值\n", i+1)
	}
	fmt.Println("继续执行close(resultChan)代码")
	close(resultChan)
	RangeResult(resultChan)
	fmt.Println("main线程结束...")
}

结果

取出exit中的第1个值
取出exit中的第2个值
取出exit中的第3个值
取出exit中的第4个值
取出exit中的第5个值
取出exit中的第6个值
取出exit中的第7个值
取出exit中的第8个值
继续执行close(resultChan)代码
3
6
...
595
630
666
第1个协程工作结束!
第3个协程工作结束!
第5个协程工作结束!
第8个协程工作结束!
第4个协程工作结束!
第6个协程工作结束!
第7个协程工作结束!
703
...
1431
1485
1540
1596
第2个协程工作结束!
1653
1711
...
497503
500500
498501
499500
main线程结束...

Go语言学习篇06

Channel 使用Details

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

2)默认情况下channel是双向的

3)channel只读和只写的最佳案例

4)使用select可以解决从管道取数据的阻塞问题

5)Goroutine中使用recover,解决协程中出现的panic,导致程序崩溃问题

只读、只写是channel的属性,因此不会改变channel的类型

  • 男的是人
  • 女的也是人

3)代码

var intChan chan<- int = make(chan int, 1) // 只写
var intChan <-chan int = make(chan int, 1)// 只读

Go语言学习篇06

4)代码

package main

import (
	"fmt"
)

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.Sprint(i)
	}

	// 传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
	// close() 实际开发中我们无法明确什么时候关闭该管道,使用select

	for true {
		select {
		// 如果管道没有关闭,也不会导致一直死锁而导致deadlock
		// 会自动的到下一个case匹配
		case value := <- intChan :
			fmt.Printf("从intChan读取数据%d\n", value)
		case value := <- stringChan :
			fmt.Printf("从intChan读取数据%v\n", value)
		default:
			fmt.Println("什么也没有,不取了...")
			return
		}
	}
}

结果

从intChan读取数据hello0
从intChan读取数据0
从intChan读取数据1
从intChan读取数据2
从intChan读取数据3
从intChan读取数据hello1
从intChan读取数据hello2
从intChan读取数据4
从intChan读取数据5
从intChan读取数据6
从intChan读取数据hello3
从intChan读取数据7
从intChan读取数据hello4
从intChan读取数据8
从intChan读取数据9
什么也没有,不取了...

5)代码

package main

import (
	"fmt"
	"time"
)

func SayHello() {
	for i := 0; i < 10; i++ {
		fmt.Println("hello world +", i+1)
	}
}

func Test() {
	//必须写在最前端
	defer func() {
		// 捕获抛出的panic
		if error := recover(); error != nil {
			fmt.Println("Test协程发生错误,error:", error)
		}
	}()
	// 定义一个map
	var myMap map[int]string
	myMap[0] = "golang"
}

func main() {
	go SayHello()
	// Test协程有问题,直接影响到SayHello协程,整个程序崩溃
	go Test()
	// 防止主线程直接跑路了
	time.Sleep(time.Second)
}

结果

hello world + 1
hello world + 2
hello world + 3
hello world + 4
hello world + 5
hello world + 6
hello world + 7
hello world + 8
hello world + 9
hello world + 10
Test协程发生错误,error: assignment to entry in nil map

结果

从intChan读取数据hello0
从intChan读取数据0
从intChan读取数据1
从intChan读取数据2
从intChan读取数据3
从intChan读取数据hello1
从intChan读取数据hello2
从intChan读取数据4
从intChan读取数据5
从intChan读取数据6
从intChan读取数据hello3
从intChan读取数据7
从intChan读取数据hello4
从intChan读取数据8
从intChan读取数据9
什么也没有,不取了...
上一篇:golang context的WithTimeout实现调用自定义函数超时处理


下一篇:Go(进阶):06---Go语言实现Raft算法(简易版,不带日志复制)