案例一
请完成goroutine和channel协同工作的案例,具体要求:
(1).开启一个writeData协程,向管道intChan中写入50个整数.
(2).开启一个readData协程,从管道intChan中读取writeData写入的数据
(3).注意: writeData和readDate操作的是同一个管道
(4).主线程需要等待writeData和readDate协程都完成工作才能退出
示例图如下:

代码如下:
- package main
-
- import (
- "fmt"
- )
-
- //write data
- func WriteData(intChan chan int) {
- for i := 0; i < 50; i++ {
- intChan <- i //放入数据
- fmt.Printf("write 数据=%v\n", i)
- }
- //关闭管道
- close(intChan)
- }
- //read data
- func ReadData(intChan chan int, exitChan chan bool) {
- for {
- v, ok := <-intChan
- if !ok {
- break
- }
- fmt.Printf("read 读取到的数据=%v\n", v)
- }
- //读取完成后,任务完成
- exitChan <- true
- close(exitChan)
- }
- func main() {
- //创建两个管道
- intChan := make(chan int, 50)
- exitChan := make(chan bool, 1)
- go WriteData(intChan)
- go ReadData(intChan, exitChan)
- //判断
- for {
- _, ok := <- exitChan
- if !ok {
- break
- }
- }
- }
阻塞
func main(){ intChan := make(chan int, 10) // 10->50的话数据一下就放入了 exitChan:=make(chan bool,1) go writeData(intChan) go readData(intChan,exitChan) //就是为了等待..readData协程完成 for _ = range exitChan{ fmt.Println("ok...") } }问题:
如果注销掉 go readData(intChan,exitChan),程序会怎么样?
答:
如果只是向管道写入数据,而没有读取(如果编译器(运行),发现一个管道只有写,而没有读,则该管道会阻塞;写管道和读管道的频率不一致,不影响),就会出现阻塞而dead lock,原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在writeData的 ch<-i
案例二
要求:
(1).启动一个协程,将1~2000的数放入到一个channel中,比如numChan
(2).启动8个协程,从numChan取出数(比如n),并计算1+...+n的值,并存放到resChan
(3).最后8个协呈协同完成工作后,再遍历resChan,显示结果[如res[1]=1..res[10]=55..]
注意:考虑resChan chan int是否合适?
案例三
goroutine+ channel配合完成排序,并写入文件
要求:
(1).开一个协程writeDataToFile,随机生成1000个数据,存放到文件中
(2).当writeDataToFile完成写1000个数据到文件后,让sort协程从文件中读取1000个文件,并完成排序,重新写入到另外一个文件
(3).考察点:协程和管道+文件的综合使用
(4).功能扩展:开10个协程writeDataToFile,每个协程随机生成1000个数据,存放到10文件中
(5).当10个文件都生成了,让10个sort协程从10文件中读取1000个文件,并完成排序,重新写入到10个结果文件
案例四
(1).创建一个 Person 结构体 [Name,Age,Address]
(2).使用rand方法配合随机创建10个Person实例,并放入到channel中
(3).遍历channel,将各个Person实例的信息显示在终端
- package main
-
- import (
- "fmt"
- "math/rand"
- "strconv"
- )
-
- type Person struct {
- Name string
- Age int
- Address string
- }
- //创建Person实例,并写入chan
- func WriteChan(personChan chan Person) {
- for i := 0; i < 100; i++ {
- //把person放入管道中
- personChan <- Person {
- Name : "Person" + strconv.Itoa(rand.Intn(100)),
- Age : rand.Intn(100),
- Address : "成都市华府大道" + strconv.Itoa(rand.Intn(100)) + "号",
- }
- fmt.Println("wtite data=", i)
- }
- //关闭管道
- close(personChan)
- }
- //读取chan数据,并显示到终端
- func ReadChan(personChan chan Person, exitChan chan bool) {
- i := 0
- for {
- i++
- v , ok := <- personChan
- if !ok {
- break
- }
- fmt.Printf("第%v个person数据的Name=%v,Age=%v, Address=%v\n", i,
- v.Name, v.Age, v.Address)
- }
- 读取完成后,任务完成
- exitChan <- true
- close(exitChan)
- }
- func main() {
- //创建两个管道
- personChan := make(chan Person, 100)
- exitChan := make(chan bool, 1)
- go WriteChan(personChan)
- go ReadChan(personChan, exitChan)
- for {
- _, ok := <- exitChan
- if !ok {
- break
- }
- }
- }
案例五
需求:
要求统计 1 ~ 200000 的数字中,哪些是素数?
分析思路:
传统的方法,就是使用一个循环,循环的判断各个数是不是素数
使用并发并行的方式,将统计素数的任务分配给多个( 4 个) gorontine去完成,完成任务时间短
示意图如下:
代码如下:
- package main
-
- import (
- "fmt"
- "time"
- )
-
- //向intChan放入1~8000
- func putNum(intChan chan int) {
- for i := 1; i <= 1000; i++ {
- intChan<- i
- }
- //关闭intChan
- close(intChan)
- }
- //从intChan取出并判断是否素数,如果是,就放入primeChan中,当前协程完成后,需设置exitChan退出标识
- func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
- var flag bool // 是不是素数标识符
- for {
- time.Sleep(time.Millisecond * 10)
- //从intChan取出数据,并判断是不是素数
- num, ok := <-intChan
- if !ok {
- break
- }
- flag = true
- for i := 2; i < num; i++ {
- if num % i == 0 { // 说明不是素数
- flag = false
- break
- }
- }
- if flag {
- //将结果放入primeChan
- primeChan<- num
- }
- }
- fmt.Println("有一个primeChan取不到数据,退出")
- //在这里还不能关闭primeChan,因为不知道哪一个primeChan还在执行
- //向exitChan放入true
- exitChan<- true
- }
- func main() {
- //创建3个管道
- intChan := make(chan int, 1000) //存放1~8000的数
- primeChan := make(chan int, 1000) //放入结果
- exitChan := make(chan bool, 4) //标识退出的管道
- //开启一个协程,向intChan放入1~8000个数
- go putNum(intChan)
- //开启4个协程,从intChan取出并判断是否素数,如果是,就放入primeChan中
- for i := 1; i <= 4; i++ {
- go primeNum(intChan, primeChan, exitChan)
- }
-
- //判断primeChan是否执行完毕,并关闭prizeChan
- go func(){
- for i := 1; i <= 4; i++ {
- <-exitChan
- }
- //当从exitChan中取出4个数后,就可以关闭primeChan了
- close(primeChan)
- }()
- //遍历primeChan
- for {
- res, ok := <-primeChan
- if !ok {
- break
- }
- //输出
- fmt.Printf("素数:%d\n", res)
- }
- fmt.Println("main主线程退出")
- }
看看执行的速度:
- package main
-
- import (
- "fmt"
- "time"
- )
-
- //向intChan放入1~8000
- func putNum(intChan chan int) {
- for i := 1; i <= 80000; i++ {
- intChan<- i
- }
- //关闭intChan
- close(intChan)
- }
- //从intChan取出并判断是否素数,如果是,就放入primeChan中,当前协程完成后,需设置exitChan退出标识
- func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
- var flag bool // 是不是素数标识符
- for {
- // time.Sleep(time.Millisecond * 10)
- //从intChan取出数据,并判断是不是素数
- num, ok := <-intChan
- if !ok {
- break
- }
- flag = true
- for i := 2; i < num; i++ {
- if num % i == 0 { // 说明不是素数
- flag = false
- break
- }
- }
- if flag {
- //将结果放入primeChan
- primeChan<- num
- }
- }
- fmt.Println("有一个primeChan取不到数据,退出")
- //在这里还不能关闭primeChan,因为不知道哪一个primeChan还在执行
- //向exitChan放入true
- exitChan<- true
- }
- func main() {
- //创建3个管道
- intChan := make(chan int, 1000) //存放1~8000的数
- primeChan := make(chan int, 1000) //放入结果
- exitChan := make(chan bool, 4) //标识退出的管道
-
- //开始时间
- start := time.Now().Unix()
- //开启一个协程,向intChan放入1~8000个数
- go putNum(intChan)
- //开启4个协程,从intChan取出并判断是否素数,如果是,就放入primeChan中
- for i := 1; i <= 4; i++ {
- go primeNum(intChan, primeChan, exitChan)
- }
-
- //判断primeChan是否执行完毕,并关闭prizeChan
- go func(){
- for i := 1; i <= 4; i++ {
- <-exitChan
- }
- //结束时间
- end := time.Now().Unix()
- fmt.Println("使用协程耗费的时间:", end - start)
- //当从exitChan中取出4个数后,就可以关闭primeChan了
- close(primeChan)
- }()
- //遍历primeChan
- for {
- _, ok := <-primeChan
- if !ok {
- break
- }
- //输出
- //fmt.Printf("素数:%d\n", res)
- }
- fmt.Println("main主线程退出")
- }
结论:使用go协程后,执行速度比普通方法提高至少4倍
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- //管道可以声明为只读或只写
- //1.在默认的情况下,管道是双向的
- // var chan1 chan int//可读可写
- //2.声明为只写
- var chan2 chan<- int
- chan2 = make(chan int, 3)
- chan2<- 33
- // num := <-chan2 //error
- fmt.Println("chan2=", chan2)
- //2.声明为只读
- var chan3 <-chan int
- chan3 = make(chan int, 3)
- num := <-chan3
- //chan3<- 33//error
-
- fmt.Println("num=", num)
- }
- 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
- strChan := make(chan string, 5)
- for i := 0; i < 5; i++ {
- strChan <- "hello" + fmt.Sprintf("%d", i)
- }
-
- //传统的方式在遍历管道时,如果不关闭会阻塞而导致deallock
-
- //问题:在实际开发中,有可能不好确定在什么情况下去关闭管道
- //可以使用select方式解决
- for {
- select {
- //注意,如果intChan一直没有关闭,不会一直阻塞而deallock,会自动到下一个case匹配
- case v := <- intChan :
- fmt.Printf("从intChan取出数据:%v\n", v)
- case v := <- strChan :
- fmt.Printf("从strChan取出数据:%s\n", v)
- default :
- fmt.Println("取不到数据了,程序员可以添加自己的逻辑代码")
- return
- }
- }
- }
如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在 goroutine 中使用 recover来捕获 panic ,进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续执行
- package main
-
- import (
- "fmt"
- "time"
- )
-
- func sayHello() {
- for i := 0; i < 10; i++ {
- time.Sleep(time.Second)
- fmt.Println("hello")
- }
- }
-
- func test() {
- //这里可以使用defer + recover
- defer func () {
- //捕获test抛出的异常
- if err := recover(); err != nil {
- fmt.Println("test() 发生错误", err)
- }
- }()
- var map1 map[int]string
- map1[0] = "go" //error
- }
-
- func main() {
- go sayHello()
- go test()
-
- for i := 0; i < 10; i++ {
- fmt.Println("main hi")
- time.Sleep(time.Second)
- }
- }