Go-goroutine(协程)和channel(管道)

Go-goroutine(协程)

1.goroutine基本介绍

1.1 进程和线程说明:
1)进程就是程序在操作系统中的一次执行过程,是系统进行资源分配(CPU时间、内存等)和调度的基本单位;有独立的内存空间,切换开销大。

2) 线程是进程的一个执行实例/流,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。同一进程中的多线程共享内存空间,线程切换代价下;多线程通信方便;从内核层面来看线程其实是一种特殊的进程,它跟父进程共享了打开的文件文件系统信息,共享了地址空间信号处理函数
3)一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。
4)一个程序至少有一个进程,一个进程至少有一个线程
在这里插入图片描述
1.2 并发和并行
1)多线程程序(进程中含有多个执行实例/线程)在单核上运行,就是并发
2)多线程程序在多核上运行,就是并行
3)示意图:
在这里插入图片描述
并发的特点:
(1)多个任务/线程作用在一个CPU
(2)从微观角度,在一个时间点上其实只有一个任务/线程在执行;每个线程执行10ms,进行轮循操作处理机制。
并行的特点
(1)多个任务作用在多个CPU上,有效利用了多核电脑的优势
(2)从微观角度,其本质上是在一个时间点上,同时执行多个任务/线程
(3)并行的运行速度更快

4)小结:
(1)在此介绍的并发外部并发不是同一概念,外部并发是否同时完成线程任务取决于实际情况。
(2)相比其他语言,Go语言自带能实现多任务/线程并行处理机制,能够更有效地利用多核电脑的优势。这是Go语言的凸显有优势。

1.3 Go协程和Go主线程
1)Go主线程(有程序员直接称为线程/也可以理解为进程):一个Go线程上,可以起多个协程(轻易支持上万个协程),协程可以理解为:协程是轻量级的线程(编译器底层进行了优化,共享了相同信息,减轻了存储内存)
一个goroutine默认占用内存:2KB,而线程默认占用为8MB;
在切换层面:goroutine只涉及三个寄存器(PC/SP/DX)的值修改,而线程涉及模式切换(从用户态切换到内核态)、16个寄存器(PC/SP等)的刷新
2)Go协程的特点
(1)有独立的栈空间
(2)共享程序堆空间
(3)协程是轻量级的线程
(4)调度由用户控制
在这里插入图片描述
CPU上并发执行多个线程实质是对各个线程进行资源分配(执行时间),用户线程切换等无需内核态进行切换,有效利用内核态线程所分配的资源。

2.goroutine快速入门案例

编写一个程序,完成如下功能:
1)在主线程(可以理解为进程)中,开启一个goroutine,该协程每隔一秒输出"hello,world"
2)在主线程中也隔一秒输出"hello,world",输出10次后,退出程序
3)要求主线程和goroutine同时执行
代码实现:
使用关键字go,开辟一个协程

func test() {
	for i := 1; i < 11; i++ {
		// fmt.Printf("hello,world %v\n", i)
		fmt.Println("test() hello,world" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}

}
func main() {
	go test() //关键字go ,开辟一个协程
	for i := 1; i < 11; i++ {
		// fmt.Printf("hello,world %v\n", i)
		fmt.Println("main() hello,world" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

Output:
同时执行。在这里插入图片描述

主线程和协程执行流程图:

在这里插入图片描述
快速入门小结:
1)主线程是一个物理线程直接作用在cpu上的。是重量级的,非常耗费cpu资源。
2)协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3)Golang的协程机制是Go语言的重要特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大。相较而言,突出了Golang在并发上的优势。

3.goroutine的调度模型

MPG模式基本介绍:
1)M:操作系统的主线程(物理线程/内核线程)
2)P:协程执行所需要的的上下文
3)G:协程
在这里插入图片描述
MPG模式运行的状态1:
在这里插入图片描述
1)当前有三个主线程M,如果该三个主线程都作用在一个CPU上,就是并发机制;若在不同的CPU上运作,则是并行机制
2)M1,M2,M3,正在执行一个协程G,M1的协程队列有3个协程,M2的协程队列有2个协程,M3的协程队列有2个协程在等待。
3)从图中可知:Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程。
4)其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能就耗光CPU资源。


MPG模式运行的状态2:(动态运行)
在这里插入图片描述
1)分成两个部分来看
2)原来的运行情况(图左)是M0主线程正在执行G0协程,另外三个协程在队列等待。
3)如果G0协程阻塞,例如读取文件或数据库等
4)这是Golang就会创建M1主线程(也可能是从已有的线程池中取出M1),将等到中的三个协程挂到M1下开始执行,同时M0主线程下的G0协程仍然执行(文件的io的读写)。
5)这种MPG调度模式,既可以让G0协程执行,同时也不会让队列的其它协程一直阻塞,仍然能并发/并行执行。
6)等G0协程不阻塞了,M0会被放到空闲的主线程中继续执行(从已有的线程池中取出),同时G0协程又会被唤醒。

4.设置Golang运行的cpu数

使用到runtime包中的func NumCPU()func GOMAXPROCS()函数

func NumCPU
func NumCPU() int
NumCPU返回本地机器的逻辑CPU个数。

func GOMAXPROCS
func GOMAXPROCS(n int) int
GOMAXPROCS设置可同时执行的最大CPU数,并返回先前的设置若 n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过 NumCPU 查询。本函数在调度程序优化后会去掉。

func main() {
	cpuNum := runtime.NumCPU()
	fmt.Println("cpunum =", cpuNum)

	// 设置使用多个CPU
	primeNum := runtime.GOMAXPROCS(cpuNum - 2)
	fmt.Println("primeNum =", primeNum)
}

Output:
cpunum = 12
primeNum = 12

注:
go1.8后,默认让程序运行在多核上,可以不用设置了。


Go-Channel(管道)

1.channel基本介绍

需求分析:
案例:
1)需求:计算1-200的各个数的阶乘,并且各个数的阶乘放入到map中
2)要求使用goroutine完成

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

func test(n int) {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	myMap[n] = res //fatal error: concurrent map writes

}

func main() {
	for i := 1; i <= 200; i++ {
		go test(i)
	}
	time.Sleep(10*time.Second)
	
	// 显示结果
	for i, v := range myMap {
		fmt.Printf("map[%v]=%v\n", i, v)
	}
}

Output:
fatal error: concurrent map writes

分析:
1)使用goroutine完成,效率高,但是会出现并发/并行安全问题。这是由于多个协程都在向同一个map进行写操作。
2)这就引出了不同的goroutine如何通信的问题
3)在运行某个程序时,欲知道是否存在资源竞争问题,在编译该程序时增加一个参数-race即可

go build -race main.go
main.exe

WARNING: DATA RACE

针对上述问题的解决方案:
1)定义全局变量加锁(互斥锁)同步
2)channel


使用全局变量加锁同步改进程序:
要是使用到sync包中定义的定义的结构体Mutex(互斥)的func (*Mutex) Lock func (*Mutex) Unlock方法。

sync包提供了基本的同步基元,如互斥锁。sync包中函数\方法大部分都是适用于低水平程序线程高水平的同步使用channel通信更好一些。

type Mutex
type Mutex struct {
// 包含隐藏或非导出字段
}
Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁

func (m *Mutex) Lock()
Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁。
func (m *MUnlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。utex) Unlock()


加锁同步改进程序goroutine工作机制示意图:
在这里插入图片描述
改进代码:

var (
	myMap = make(map[int]int, 10)
	// 声明一个全局的互斥锁
	// 来源于sync包定义的Mutex结构体
	// sync:synchronize 同步 Mutex:互斥
	lock sync.Mutex
)

func test(n int) {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	// 加锁
	lock.Lock()
	myMap[n] = res //fatal error: concurrent map writes
	// 解锁
	lock.Unlock()
}

func main() {
	for i := 1; i <= 20; i++ {
		go test(i)
	}

	// 阻塞程序10s,所有协程都完成
	time.Sleep(10 * time.Second)

	// 显示结果
	// 读取map 时也需要上互斥锁
	// 由于goroutine可能存在滞留,在读取文件时,同时存在写入操作,导致资源竞争
	lock.Lock()
	for i, v := range myMap {
		fmt.Printf("map[%v]=%v\n", i, v)
	}
	lock.Unlock()
}


为什么需要Channel
前面使用全局变量加锁同步来解决goroutine的通讯并不完美,加锁操作适合于低水平程序线程,高水平同步使用channel通信更好些。
1)主线程在等待所有goroutine全部完成的时间很难确定
2)如果主线程休眠时间长了,加长了等待时间,资源浪费;若等待时间短了可能还有goroutine处于工作状态,这时主线程的退出会销毁协程
3)通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
4)因此引出了新的通讯机制-channel


channel介绍
1)channel本质就是一个数据结构-队列
2)数据先进先出(First in First out,FIFO)
3)管道本身线程安全, 多协程goroutine访问时,不需加锁。
4) channel是有类型的,一个string类型的channel只能存放string类型的数据。若要想一个管道同时存放多种数据类型,则设成成interface{}类型即可。(尽量不要混用类型,若混用调取数据还需断言操作)


2.管道(channel)-基本使用

2.1 定义/声明channel
var 变量名 chan 数据类型chan为关键字。
举例:
var intChan chan int,intChan为存放int数据的管道
var mapChan chan map[int]string,mapChan存放map[int]string类型数据的管道
var perChan chan *Person,perChan为存放Person结构体指针类型的管道

说明
1)channel是引用类型
2)channel和map一样需初始化后才能写入数据,即需make处理后才能使用
3)管道是有类型的,严格按照类型匹配
4)管道的容量是定死了的,不像map能动态增加;管道的工作机制是取出一个数据,其长度自动减小,容量不发生变化。因此,可以取出一个数据,再往管道里加入新的数据,实现类似动态增加的效果。

2.2 写入/读取管道数据
channel的写入和读取操作都很形象化;写/推入使用->,读/推出使用<-.
入门举例:
var intChan = make(chan int,3)
写入:intChan <- 10;读出:num:= <- intChan

入门案例1:

func main() {
	// 演示管道的使用
	//1.创建一个可以存放三个int类型的管道
	var intChan = make(chan int, 3)
	// var intChan chan int
	// intChan = make(chan int,3)

	// 2.intChan(引用类型) 本身存放内容是地址
	fmt.Printf("intChan=%v,intChan的地址是=%p\n", intChan, &intChan)
	// intChan=0xc0000d4080,intChan的地址是=0xc0000ce018

	// 3.向管道写入数据
	intChan <- 10
	num := 20
	intChan <- num
	intChan <- 30
	// 注意,管道中写入/推入数据不能超过管道的容量
	// intChan <- 40 //运行则会报错,
	// fatal error: all goroutines are asleep - deadlock!
	
	<-intChan
	intChan <- 40
	//运行成功,管道流动,取出一个后,在写入一个未超出容量
	

	// 4.查看管道的长度和容量
	fmt.Printf("intChan的len=[%v],cap=[%v]\n", len(intChan), cap(intChan))

	// 从管道中读取数据
	num1 := <-intChan
	fmt.Println("num2=", num1)
	fmt.Printf("intChan的len=[%v],cap=[%v]\n", len(intChan), cap(intChan)) //2,3
	// 读出数据后管道的长度减一,容量不变

	// 6.在没有使用goroutine的情况下,若管道数据已经全部取出,再取数据同样会报deadlock
	num2 := <-intChan
	num3 := <-intChan
	fmt.Println("num2=", num2, "num3=", num3)

	num4 := <-intChan
	fmt.Println("num2=", num2, "num3=", num3, "num4", num4) //deadlock 

}

入门案例2:
对于存放多种数据类型的管道,在提取某个数据,并编译使用该数据的字段或/方法时,需使用到类型断言。

type Cat struct {
	Name string
	Age  int
}

func main() {
	allChan := make(chan interface{}, 5)
	cat1 := Cat{
		Name: "tom",
		Age:  5,
	}
	cat2 := Cat{"tom~", 4}

	allChan <- 10
	allChan <- "hello"
	allChan <- cat1
	allChan <- cat2

	//由于管道的本质是队列,若想取出管道中的某个非首位数据,
	// 需先将前的数据退出管道
	<-allChan //直接退出,进入GC垃圾回收器
	<-allChan
	// 取得管道中原先第三个数据
	cat11 := <-allChan
	fmt.Printf("cat11类型=%T,cat11=%v\n", cat11, cat11)
	// cat11类型=main.Cat,cat11={tom 5} 在运行层面 获知cat11的类型是Cat结构体

	// fmt.Println("cat11的name:", cat11.Name) //报错
	//  (type interface{} has no field or method Name
	//从编译层面,cat11仍然是interface{}类型,interface{}没有字段的

	// 因此需要使用类型断言,即可编译通过
	cat12, ok := cat11.(Cat)
	if ok {
		fmt.Println("cat1的name:", cat12.Name)
	}
}

2.3 channel的关闭和遍历
2.3.1 channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以读取channel中存在的数据。

func close(c chan<- Type)
内建函数close关闭通道,该通道必须为双向的或只发送的。它应当只由发送者执行,而不应由接收者执行,其效果是在最后发送的值被接收后停止该通道。在最后的值从已关闭的信道中被接收后,任何对其的接收操作都会无阻塞的成功。对于已关闭的信道,语句
x, ok := <-c
还会将ok置为false。

案例演示:

unc main() {

	intChan := make(chan int, 3)
	intChan <- 10
	intChan <- 20

	// 关闭通道,使用内置函数close
	close(intChan)
	// 此时无法再对通道写输入数据
	// intChan <- 30 //报错:panic: send on closed channel

	// 当通道关闭后可以取其存在排队的值
	num1 := <-intChan
	num2 := <-intChan
	fmt.Println("num1=", num1, "num2=", num2) //num1= 10 num2= 20

	// 当最后值从已关闭的通道中被接收后,任何对其接收操作都会无阻塞成功
	num3 := <-intChan
	fmt.Println("num3=", num3) // num3= 0

	// 但对于空值关闭通道的的接收操作,其判断变量还是会置为false
	num4, ok := <-intChan
	if !ok {
		fmt.Println("channel is closed and empty")
		fmt.Println("num4=default value:", num4)
	}

Output:
num1= 10 num2= 20
num3= 0
channel is closed and empty
num4=default value: 0


2.3.2 channel的遍历
channel支持for-range的方式进行遍历,由于channel的长度是在改变的使用传统的for遍历就显得不合时宜。
需注意的细节:
1)在遍历时,如果channel没有关闭,则会出现deadlock的错误。这是由于在管道没有关闭情况下,使用for-range遍历,当遍历完管道所有数据后,底层运行仍然在死等下一个内容,不会主动退出。
2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出。

func main() {

	intChan := make(chan int, 50)
	// 向管道中写入数据
	for i := 0; i < 50; i++ {
		intChan <- i * 2 //写入50个偶数
	}

	// 由于管道本身进行一次读操作后自身长度会减一
	// 1.遍历管道使用传统的for循环不合适
	intSlice := make([]int, 50)
	for i := 0; i < len(intChan); i++ {
		intSlice[i] = <-intChan
	}
	fmt.Println(intSlice)
	// 输出结果,只遍历了管道的前一半数据

	// 改进方法:
	// a := len(intChan)
	// for i := 0; i < a; i++ {
	// 	intSlice[i] = <-intChan
	// }
	// fmt.Println(intSlice)
	// 使用传统的for方法遍历 不需关闭通道,因其限制死了遍历的次数。

	//2. 使用for-range 进行遍历
	// 若没有关闭管道,在遍历完所有管道内容后,底层编译会依然死等下一个内容,不会退出
	// 故会报错:fatal error: all goroutines are asleep - deadlock!
	close(intChan)

	// channel 本质上是队列,故其使用for-range遍历时只返回值,不存在下标
	// channel 的推出/读出 严格按照先进先出FIFO
	for v := range intChan {
		fmt.Println("v=", v)
	}
}

2.3.3 channel遍历和关闭案例小结
1)案例中对管道的遍历,就是等价于从管道中取数据,即:<- ch
2)注意需要close管道,否则就会出现deadlock
3)在for-range 管道时,当遍历到最后的时候,发现管道关闭,就结束读取数据的工作,正常退出。
4)在for-range管道时,当遍历到最后,发现管道没有关闭,程序会认为还有数据继续写入,因此会等待,如果程序持续没有数据写入们就会出现死锁deadlock。


2.4 goroutine和channel应用案列
案列一:
使用同一通道完成读写操作

func ReadData(c1 chan int, c2 chan bool) {
	for {
		v, ok := <-c1
		if !ok {
			break
		}
		fmt.Println("v=", v)
	}
	c2 <- true
	close(c2)
}

func main() {
	// 定义两个管道
	// 管道1 用于读写
	intChan := make(chan int, 25)
	// 管道2 用于判断读操作结束
	exitChan := make(chan bool, 1)

	t1 := time.Now()
	//write data
	go func(c1 chan int) {
		for i := 0; i < 25; i++ {
			c1 <- i * 2
			fmt.Println("write v=", i*2)
		}
		close(c1)
	}(intChan)

	// read data
	go ReadData(intChan, exitChan)
	t2 := time.Now()
	fmt.Printf("goroutine consume time=%v\n", t2.Sub(t1)) //0s

	// 当exitChan管道中读取到数据,则读取操作结束
	for {
		if _, ok := <-exitChan; ok {
			break
		}
	}
	t3 := time.Now()
	fmt.Println("读写操作完成,共耗时=", t3.Sub(t1))
}

思路示意图:

在这里插入图片描述


应用案列2:阻塞
1)注销上述案例一代码go readData(intChan,exitChan),并将intChan的容量改为10,则再向管道写入数据25个数据就会阻塞,造成deadlock现象。
2)当管道容量小于预计写入数据量时,具有相应的读操作协程,管道也不会发生无意义堵塞;并且Golang支持异步操作:支持写入管道频率和读管道频率不一致,底层编译器会产生有意义的堵塞,并不会发生死锁现象。
代码:

func ReadData(c1 chan int, c2 chan bool) {
	for {
		v, ok := <-c1
		if !ok {
			break
		}
		// 每次读出操作间隔一秒,而写入操作是一次性写入的
		// 证明Golang支持异步操作
		time.Sleep(time.Second)
		fmt.Println("v=", v)
	}
	c2 <- true
	close(c2)
}

func main() {
	// 定义两个管道
	// 管道1 用于读写
	intChan := make(chan int, 10)
	// 管道2 用于判断读操作结束
	exitChan := make(chan bool, 1)

	t1 := time.Now()
	//write data,管道容量小于写入数据量
	go func(c1 chan int) {
		for i := 0; i < 25; i++ {
			c1 <- i * 2
			fmt.Println("write v=", i*2)
		}
		close(c1)
	}(intChan)

	// read data
	go ReadData(intChan, exitChan)
	// go 调用后会直接向下执行代码块,故t2.Sub(t1)毫无意义
	//t2 := time.Now()
	//fmt.Printf("goroutine consume time=%v\n", t2.Sub(t1)) //0s

	// 当exitChan管道中读取到数据,则读取操作结束
	for {
		if _, ok := <-exitChan; ok {
			break
		}
	}
	t3 := time.Now()
	fmt.Println("读写操作完成,共耗时=", t3.Sub(t1))
}

应用案列3:统计素数
需求:统计1-100000的数字中,哪些是素数?
思路分析:
1)传统的方法,就是一个循环,循环的判断各个数是不是素数就ok了
代码:

//要求打印100以内的素数,每行显示5个,最后素数的和
func primeNumber(n1 int) int {
	count := 0
	for i := 2; i <= n1-1; i++ {
		count++
		if n1%i == 0 { //不是素数
			break
		}
	}
	if count == n1-2 {
		return n1
	} else {
		return 0
	}

}

func main() {
	sum := 0
	count := 0
	for i := 1; i <= 100; i++ {
		a := primeNumber(i)
		if a == 0 {
			continue
		} else {
			count++
			sum += i
			if count%5 != 0 {
				fmt.Printf("%d\t", i)
			} else {
				fmt.Printf("%d\r\n", i) //每行显示第五个换行
			}
		}
	}
	fmt.Println(sum)
}

2)使用并发/并行的方式,将统计素数的任务分配给多个(4)个goroutine去完成,完成任务时间更短。
(分析思路):
在这里插入图片描述
代码:

// 需求:统计1-100000的数字中,哪些是素数?
//并将这些素数排序好后写入文件中

// 开启判断素数协程
// 要求,是素数就放入到primeChan管道中
func PrimeNum(c1, c2 chan int, c3 chan bool) {
	for {
		v, ok := <-c1
		// time.Sleep(time.Millisecond * 10)
		if !ok {
			break
		}
		flag := true //标识是否为素数
		for i := 2; i <= v-1; i++ {
			if v%i == 0 {
				flag = false
				break
			}
		}
		if flag {
			c2 <- v //是素数就写入primeChan
		} else {
			continue //不是素数继续循环
		}
	}
	//有一个协程取不到数据退出了
	// 向退出标识管道exitChan写入一个标识
	fmt.Println("有一个primeNum协程取不到数据,退出")
	c3 <- true
}

func main() {
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 20000) //放入结果
	exitChan := make(chan bool, 4)     //标识退出的管道

	// 开启一个协程向intChan放入1-8000个数
	go func(c1 chan int) {
		for i := 1; i <= 100000; i++ {
			c1 <- i

		}
		close(c1) //读完数据后,关闭管道intChan
	}(intChan)

	// 开辟四个primeNum协程
	for i := 0; i < 4; i++ {
		go PrimeNum(intChan, primeChan, exitChan)
	}

	//开辟一个协程用于判断exitChan是否写入4个值,
	// 即素数判断协程们是否都关闭了,并关闭primeChan
	go func() {
		for i := 0; i < 4; i++ {
			<-exitChan
		}
		close(primeChan)
	}()

	// 将得出的素数进行排序,然后写进文件
	// 定义一个文件写入完标识,用于阻塞主线程
	Flag := false
	go func(c2 chan int) {
		intSlice := make([]int, 0) //存放素数
		for {
			v, ok := <-c2
			if !ok {
				break
			}
			intSlice = append(intSlice, v)
		}
		// 进行升序排序
		sort.Ints(intSlice)
		// 写入文件处理:
		// 打开/创建一个文件
		filePath := "E:\\goproject\\src\\go_code\\chapter15\\ChannelPrimeNumber\\Channel_Primer\\素数.txt"
		// 加入os.O_TRUNC 每次运行都会重新清除文件上传写入内容
		file, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_APPEND|os.O_TRUNC, 0666)
		if err != nil {
			log.Fatal(err)
		}
		// 创建带缓存的writer
		writer := bufio.NewWriter(file)
		for _, v := range intSlice {
			str := strconv.Itoa(v) + "\n"
			writer.WriteString(str)
		}
		writer.Flush()
		Flag = true

	}(primeChan)

	// 主线程阻塞处理
	for {
		if Flag {
			break
		}
	}
	fmt.Println("操作完成")
}

小结:使用4个协程并行处理耗时会比传统方法快4倍以上。


2.5 channel 使用细节和注意事项
1)channel默认为双向通道,可以在申明时直接确定其只读或只写性质

// 1.声明只写的管道
	// 写法一:
	var intChan1 chan<- int
	intChan1 = make(chan int, 10)
	intChan1 <- 10
	// num1 := <-intChan1 //报错,cannot receive from send-only channel

	// 写法二:
	intChan3 := make(chan<- int, 10)
	// num2 := <-intChan3//报错,cannot receive from send-only channel
	for i := 0; i < 10; i++ {
		intChan3 <- i
		if i == 9 {
			fmt.Println("写入完成")
		}
	}

	//2.声明只读的管道
	// 写法一:
	var intChan2 <-chan int
	intChan2 = make(chan int, 10)
	fmt.Println(intChan2)
	// intChan2 <- 10 //报错 cannot send to receive-only type <-chan int
	// 写法二:
	intChan4 := make(<-chan int, 10)
	fmt.Println(intChan4)
	// 单独申明只读管道无实际意义

2)也可以对已经创建的双向通道,在调用函数/方法时,对该双向通道进行只读或只写性质的定义。

// 3.双向管道,单向管道的综合应用
	c := make(chan int, 10)
	exitChan := make(chan struct{}, 2)

	//只能写管道,seed-only
	go func(c1 chan<- int, c2 chan struct{}) {
		for i := 0; i < 10; i++ {
			c1 <- i
		}
		close(c1)
		var a struct{}
		exitChan <- a
	}(c, exitChan)

	// 只能读管道 receive-only
	go func(c1 <-chan int, c2 chan struct{}) {
		for {
			v, ok := <-c1
			if !ok {
				break
			}
			fmt.Println(v)
		}
		var a struct{}
		exitChan <- a
	}(c, exitChan)

	total := 0
	for _ = range exitChan { //用于判断exitChan中是否写两个数据
		total++
		if total == 2 {
			break
		}
	}
	fmt.Println("finish work")
}

3)使用select可以解决从管道取数据的阻塞问题。
//传统的方法在遍历时不关闭管道就会产生死锁deadlock
// 问题:在实际开发中,可能不好确定何时关闭管道
// 针对上述问题,可以送select方式解决

slecet的使用机制:
(1)使用select,case中的管道一直未关闭,切且不会发生deadlock
(2) 当一个case中的值取不到时,会自动匹配下一个case
(3) 当所有case都不匹配时,执行default项

	// label:
for {
		
		select {
		case v := <-intChan:
			...
		case v := <-stringChan:
			...
		......	
		default:
			.....
			return
			// break label
		}
	}

案例代码:

func main() {
	// 使用`select`可以解决从管道取数据的阻塞问题。
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}

	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}

	// label:
	for {
		// 使用select,case中的管道一直未关闭,切且不会发生deadlock
		// 当一个case中的值取不到时,会自动匹配下一个case
		// 当所有case都不匹配时,执行default项
		select {
		case v := <-intChan:
			time.Sleep(time.Second)
			fmt.Printf("从intChan中取得值%d\n", v)
		case v := <-stringChan:
			time.Sleep(time.Second)
			fmt.Printf("从stringChan中取得值%s\n", v)
		default:
			time.Sleep(time.Second)
			fmt.Println("操作完成,该处可加入业务逻辑")
			return
			// break label
		}
	}
}

4)goroutine中使用defer+recover,解决协程中出现的panic导致程序崩溃的问题。

defer操作参考:defer说明
defer+recover操作参考:错误处理机制

问题说明:为了解决协程运行中可能出现panic,造成整个程序崩溃的问题;采用defer+recover错误处理机制,之这样即使某个协程发生了问题,但是主线程序和其它协程不受影响,可以继续执行。

入门案例代码演示:

// goroutine中使用`defer + recover`,解决协程中出现的`panic`导致程序崩溃的问题。

func sayHello(c1 chan string) {
	for i := 0; i < 10; i++ {
		c1 <- "hello" + fmt.Sprintf("%d", i)
	}
	close(c1)
}
func test() {
	// 需注意:defer + recover 错误处理机制必须放在代码块开头/错误可能发生前面
	defer func() {
		// 捕获test抛出的panic
		err := recover()
		if err != nil {
			// 伪代码
			fmt.Println("test()函数报错,发送给xxx.com")
		}
	}()
	var myMap map[int]string
	myMap[0] = "err" //myMap未make error
}

func main() {
	stringChan := make(chan string, 10)
	go sayHello(stringChan)
	go test()
	for i := 0; i < 10; i++ {
		<-stringChan
	}
	fmt.Println("测试完成")
}

案例小结defer + recover 错误处理机制必须 放在代码块开头/错误可能发生前面

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值