Go Language Learning Chapter 06
unit test
traditional test method
question:
There is a function, how to confirm that the result of its operation is correct?
traditional method solutions
In the main function, call the addUpper function to see if the actual output is consistent with the expected result.
If they are consistent, the function is correct, otherwise the function has an error, and then correct the error.
the code
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)
}
}
Drawbacks of traditional methods
1) Inconvenient for testing: we need to call in main, if the project is running, we need to stop the program
2) Not conducive to management: When we test multiple functions and modules, we must write them in the main function, which is not conducive to management and clear our thinking
3) Lead to unit tests.testing测试框架
testing test framework
basic introduction
Go language comes with a lightweight testing framework testing and its own go test
commands to implement unit testing and performance testing
-
Test cases for functions can be written based on this framework
-
You can write corresponding stress test cases based on the framework
Problems that can be solved
1) Make sure that each function can be run and the result is correct
2) Make sure the performance of the written code is good
3) Timely discover logic errors单元测试
in program design or implementation , so that problems can be exposed early, and problems can be located and solved;
The性能测试
focus is to find some problems in program design , so that the program can remain stable under high concurrency
Getting started with testing
import "testing"
testing provides support for automated testing of Go packages. With go test
the command, any function of the following form can be automatically executed:
func TestXxx(*testing.T)
Where Xxx can be any alphanumeric string (but the first letter cannot be [az], must be capitalized ), used to identify the test routine (program).
Within these functions, use Error, Fail, or related methods to signal failure.
error case
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
correct case
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 Quick Start Summary
1) Test case files must _test.go
end with
2) Test cases must Test+[A-Z]
start with
3) TestAxxx( t *testing.T )
The parameter type must be*testing.T
4) There can be multiple test case functions in a test case file
5) Run test case instructions
(1) cmd>go test
[ If the operation is correct, there is no log, and when there is an error, the log will be output ]
(2) cmd>go test -v
[ If the operation is correct or wrong, output the log ]
(3) cmd>go test -v main_test.go main.go
Execute the specified single file
(4) cmd>go test -v -test.run TestAddUpper
, test a single method
6) When an error occurs, you can use it t.Fatalf
to format the error message and exit the program
7) t.Logf
, format and output the corresponding log
8) The test case function is not placed in the main function, but it is also executed. This is the convenience of the test case
9) PASS indicates that the test case runs successfully, and FALL indicates that the test case fails to run
Unit Test Synthesis Case
case requirements
1) Write a Monster structure with fields Name, Age, Skill
2) Bind a method Store to Monster, which can serialize variables and save them in files
3) Bind a Restore method to Monster, which can read the serialized Monster from the file and deserialize it into a Monster object
4) Program test call file store_test.go, write test call examples TestStore and TestRestore for testing
case code
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
}
test code
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("情报有误...")
}
}
Goroutines
Basic introduction to Goroutine
Process and Thread Description
-
process
- It is an execution process of a program in the operating system
- foreground process
- backstage process
-
thread
- A thread is an executing instance of a process
- The smallest unit of program execution
- Baidu network disk downloads multiple resources at the same time
- Multiple threads (network disk resources) in the same Baidu network disk (process ) can execute concurrently (download at the same time)
A program has at least one process, and a process has at least one thread
打开一个进程
( Baidu Netdisk )
多线程下载
( multitasking download )
Concurrency and Parallelism Explained
1) Multi-threaded programs run on a single core , that is, concurrent
2) Multi-threaded programs run on multiple cores , that is, parallel
Go coroutines and Go main thread
Single-core thread—>Multi-core thread (larger CPU)—>Synergy (smaller CPU)
-
Go coroutines are lightweight threads
-
Go main thread contains multiple coroutines (
万级别
) -
Features of coroutines
- Has independent stack space
- shared program heap space
- Scheduling is controlled by the user
- Coroutines are lightweight threads
Goroutine Quick Start
Case Description
Please write a program to complete the following functions:
1) Start a Goroutine in the main thread, which outputs "hello world" every 1 second
2) Output "hello golang" every 1 second in the main thread, exit the program after outputting 10 times
3) The main thread and the coroutine goroutine are required to execute at the same time
Goroutine Quick Start Summary
1) The main thread is a physical thread that acts directly on the CPU. Is a heavyweight and very CPU intensive.
2) The coroutine is started from the main thread, which is a lightweight thread and logical. less resource consuming
3) The coroutine mechanism of Golang is an important feature, which can easily open tens of thousands of coroutines . The concurrency mechanism of other programming languages is generally based on threads, and too many threads are opened, which consumes a lot of resources. This is the advantage of Golang in concurrency
Goroutine scheduling model
Basic introduction of MPG mode
1) M: The main thread (physical thread) of the operating system
2) P: the context environment required for coroutine execution
3) G: Coroutine
MPG mode running status
Set the number of CPUs Golang runs on
package main
import (
"fmt"
"runtime"
)
func main() {
cpuNum := runtime.NumCPU()
fmt.Println("电脑CPU数目:", cpuNum)
//可以自己设置使用多少个CPU
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Println("ok~")
}
Summarize
1) After go1.8, the program runs on multiple cores by default, so you don’t need to set it
2) Before go1.8, it needs to be set to make more efficient use of CPU
prime number problem
need:
Among the numbers from 1 to 200,000,000,000, which ones are prime numbers?
analysis of idea:
1) The traditional method uses a for loop to judge each number [cannot take advantage of multi-core]
2) Use concurrent or parallel methods to assign the task of counting prime numbers to multiple goroutines to complete
Goroutines solve problems
the code
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线程退出...")
}
result
2
3
5
7
....
89
91
93
95
97
99
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!
有一个ResultNum协程因为取不到数据,退出!// 8个
main线程退出...
channel
basic introduction
1) The essence of channel is a data structure - queue
2) Data first in first out [FIFO]
3) Thread safety, when multiple goroutines access, no need to lock, that is to say, the channel itself is thread safe
4) The channel has types , a string channel can only store string data type
resource competition problem
need:
Now calculate the factorial of each number from 1-200, and put the factorial of each number into the map. Shown at last. Need to use goroutine to complete
analysis of idea
1) Use goroutine to complete, high efficiency, but there will be concurrency and parallel problems
2) Here is a question of how different goroutines communicate , use
cmd> go build -race main.go,
cmd> main.exe
Found 3 data race(s):发现3个资源有竞争
Code
1) Use goroutine to complete
2) How to know whether there is a resource competition problem in a certain program
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)
}
}
question
- Because there is no global variable lock, there will be resource contention problems , and the code will have an error, prompting concurrent map writes
- The solution is to add a mutex
- Our factorial is large,
var result uint64接收
Solution 1: Global variable lock synchronization
资源竞争关系解决方案?
- Judging whether it is a lock state or an unlock state
- You are not allowed to enter in the locked state, please wait in line
- unlock allows access
the code
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()
}
question
1) The main thread does not know the waiting time for the coroutine to run
2) No delay time is added, it doesn’t matter whether to add a lock or not, the execution of the coroutine is incomplete
3) It is not conducive to partial reading and writing , and partial reading and writing must be locked
Solution 2: channel
channel (pipeline) - basic use
var 变量名 chan 数据类型
example
var intChan chan int 【存放int数据】
var mapChan chan map[int]string 【存放map[int]string数据】
var perChan01 chan Person
var perChan02 chan *Person
illustrate
1) channel is a reference type
2) The channel must be initialized before data can be written, that is, it can only be used after make
3) Pipelines are typed
the code
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
}
result
intChan空间中的值:0xc000104080
intChan本身的地址:0xc000006028
读取前:
channel len = 2
channel cap = 3
读取后:
channel len = 1
channel cap = 3
从管道中读取的第一个数据num= 10
Store any data variable
case two
type assertion
//需要类型断言
cat := cat02.(Cat)
fmt.Println(cat.Name)
question
Channel traversal and closure
Using the built-in channel close关闭
, the channel can only read, not write~
To buy train tickets, once the gate is closed, you can continue to buy those inside the house, but not outside the house!
Use for-range to traverse
1) If the channel is not closed, a deadlock error will occur
2) If the channel is closed, it will be traversed normally, after the traversal, exit the traversal
the code
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)
}
}
result
0
2
4
6
8
10
...
194
196
198
the code
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
}
}
}
result
-----1 //此处开始阻塞10秒
-----2 //10秒后读取成功
1
-----1
-----2
Combination of Goroutine and Channel
Solve the problem
Mutual communication between coroutines, and when to end the main thread~
the case
1) Start a writeData coroutine and write 50 integers to the pipeline intChan
2) Start a readData coroutine to read the data written by writeData from the pipeline intChan
3) Note: writeData and readData operate on the same pipeline
4) The main thread needs to wait for both coroutines to complete their work before launching (without lock)
train of thought
the code
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
}
}
}
result
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
question
The written coroutine runs very fast, intChan(chan int, 10)—>10 capacity, write 1000
The reading coroutine is very slow, although it will cause blocking problems when writing to the maximum capacity, but there will be no deadlock problem! 【The toilet is full, you can only wait first】
If there is no read coroutine, the capacity will be insufficient, and intChan will have a deadlock problem
Comprehensive Practice Questions: Difficult
the code
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线程结束...")
}
result
取出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线程结束...
Channel Usage Details
1) channel can be declared as read-only or write-only
2) By default, the channel is bidirectional
3) The best case for channel read-only and write-only
4) Use select to solve the blocking problem of fetching data from the pipeline
5) Use recover in Goroutine to solve the panic that occurs in the coroutine, causing the program to crash
只读、只写是channel的属性,因此不会改变channel的类型
- men are people
- women are human too
3) Code
var intChan chan<- int = make(chan int, 1) // 只写
var intChan <-chan int = make(chan int, 1)// 只读
4) Code
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
}
}
}
result
从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) Code
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)
}
result
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
result
从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
什么也没有,不取了...