Go Language Learning Chapter 06

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 testcommands to implement unit testing and performance testing

  1. Test cases for functions can be written based on this framework

  2. 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 testthe 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.

insert image description here

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.goend 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.goExecute 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.Fatalfto 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 )

insert image description here

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

insert image description here

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

insert image description here

insert image description here

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

insert image description here

insert image description here

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

insert image description here

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个资源有竞争

insert image description here

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

资源竞争关系解决方案?

insert image description here

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

insert image description here

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

insert image description here

insert image description here

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

insert image description here

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

insert image description here

case two

insert image description here

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

insert image description here

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

insert image description here

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
6661个协程工作结束!3个协程工作结束!5个协程工作结束!8个协程工作结束!4个协程工作结束!6个协程工作结束!7个协程工作结束!
703
...
1431
1485
1540
15962个协程工作结束!
1653
1711
...
497503
500500
498501
499500
main线程结束...

insert image description here

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)// 只读

insert image description here

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
什么也没有,不取了...

Guess you like

Origin blog.csdn.net/IT_Carter/article/details/111874498