风离不摆烂学习日志 Day3 — Go 协程与Channel管道 练习题

题目描述

使用goroutine和channe1实现一个计算int64随机数各位数和的程序
1.开启一个goroutine循环生成int64类型的随机数,发送到jobchan
2.开启24个goroutine从jobhan中取出随机数计算各位数的和,将结果发送到resultChan
3.主goroutine从resultchan取出结果并打印到终端输出

代码详解

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

/**
  使用goroutine和channe1实现一个计算int64随机数各位数和的程序
1.开启一个goroutine循环生成int64类型的随机数,发送到jobchan
2.开启24个goroutine从jobhan中取出随机数计算各位数的和,将结果发送到resultChan
3.主goroutine从resultchan取出结果并打印到终端输出
*/
type job struct { //定义结构体 job
	num int64
}
type result struct { //定义结构体 result  包含 job 中 num 和 num 各个数相加的 sum
	job *job
	sum int64
}

/**
初始化 jobChan 给定100初始空间 chan 类型为 job的指针 除了 int 类型以外 其他类型存储为其引用地址
*/
var jobChan = make(chan *job, 100)
var resultChan = make(chan *result, 100)

var wg = sync.WaitGroup{}

/**
生产者 传入 jobChan 向 其中传入 64位随机数据
*/
func producer() {
	defer wg.Done()
	// 循环生成int64的随机数,发送到jobChan
	for {
		x := rand.Int63()
		newJob := &job{
			num: x,
		}
		jobChan <- newJob
		time.Sleep(time.Second)
	}
}

func consumer() {
	defer wg.Done()
	// 从jobChan中取出随机数计算各位数的和,将结果发送给resultChan
	for {
		job := <-jobChan
		sum := getSum(job.num)
		newResult := &result{
			job: job,
			sum: sum,
		}
		resultChan <- newResult
	}

}

func getSum(num int64) (sum int64) {
	/**
	  123 3
	  12  3+2
	  1   3+2+1
	*/
	for {
		sum = sum + num%10
		num = num / 10

		if num < 1 {
			break
		}

	}

	return sum
}

func main() {
	wg.Add(1)
	go producer()
	//开启24个goroutine执行consumer
	wg.Add(24)
	for i := 0; i < 24; i++ {
		go consumer()
	}
	// 主goroutine从resultChan中取出结果并打印

	for result := range resultChan {
		fmt.Printf("value:%d sum:%d\n ", result.job.num, result.sum)
	}
	wg.Wait()
}

Go Channel 的遍历

  1. 阻塞接收数据 阻塞模式接收数据时,将接收变量作为<-操作符的左值

    data := <-ch // 执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。
    
  2. 非阻塞接收数据 使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:

func readChan(c chan int) (int, error) {
	select {
	case num := <-c:
		return num, nil
	default:
		return 0, errors.New("chan do not have data")
	}
}

// 加上超时时间
func readChanWithTimeout(c chan int) (int, error) {
	timeout := time.NewTimer(time.Microsecond * 100)

	select {
	case num := <-c:
		return num , nil
	case <-timeout.C:
		return 0, errors.New("read chan time out")
	}
}

for range 遍历

结论

在遍历时,如果channel没有关闭,则会出现deadlock的错误
在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历

package main

import (
	"fmt"
)

func main() {

	//遍历管道
	intChan2 := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan2 <- i * 2 //放入100个数据到管道
	}

	//遍历管道不能使用普通的 for 循环
	// for i := 0; i < len(intChan2); i++ {

	// }
	//在遍历时,如果channel没有关闭,则会出现deadlock的错误
	//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
	//close(intChan2)
	for v := range intChan2 { //没有下标
		fmt.Println("v=", v)
	}
}

image-20221121163532771

Go select

Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。

与switch语句可以选择任何可使用相等比较的条件相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:

    select {
    case <-chan1:
        // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
        // 如果成功向chan2写入数据,则进行该case处理语句
    default:
        // 如果上面都没有成功,则进入default处理流程
    }

在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:

  • 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。
  • 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。