为什么需要 channel

  1. 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
  2. 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作 状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5. 上面种种分析都在呼唤一个新的通讯机制-channel

channel 的基本介绍

  1. channle 本质就是一个数据结构-队列【示意图】
  2. 数据是先进先出【FIFO : first in first out】
  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
  4. channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
  5. 示意图:

    定义/声明 channel

var 变量名 chan 数据类型
举例:

var	intChan	chan	int (intChan 用于存放 int 数据)
var	mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型) var	perChan	chan	Person
var	perChan2	chan	*Person
...

说明

channel 是引用类型
channel 必须初始化才能写入数据, 即 make 后才能使用 管道是有类型的,intChan  只能写入 整数 int

管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项

package main 
import (
	"fmt"
)

func main() {
	//演示一下管道的使用
	//1. 创建一个可以存放 3 个 int 类型的管道
	var intChan chan int 
	intChan = make(chan int, 3)
	
	//2. 看看 intChan 是什么
	fmt.Printf("intChan  的值=%v intChan 本身的地址=%p\n", intChan, &intChan)
	
	//3. 向管道写入数据
	intChan<- 10
	num := 211 
	intChan<- num 
	intChan<- 50
	// intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量
	
	//4.  看看管道的长度和 cap(容量)
	fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
	
	//5. 从管道中读取数据
	var num2 int
	num2 = <-intChan 
	fmt.Println("num2=", num2)
	fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))	// 2, 3
	
	//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock 
	num3 := <-intChan
	num4 := <-intChan 
	num5 := <-intChan
	fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
}

channel 使用的注意事项

  1. channel 中只能存放指定的数据类型
  2. channle 的数据放满后,就不能再放入了
  3. 如果从 channel 取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

读写 channel 案例演示






channel 的遍历和关闭

channel 的关闭

使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然 可以从该 channel 读取数据

案例演示:

channel 的遍历

channel 支持 for–range 的方式进行遍历,请注意两个细节

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

channel 遍历和关闭的案例演示

应用实例 1


思路分析:

package main 

import (
	"fmt"
	"time"
)

//write Data
func writeData(intChan chan int) { 
	for i := 1; i <= 50; i++ {
		//放入数据
		intChan<- i 
		fmt.Println("writeData ", i)
		time.Sleep(time.Second)
	}
	close(intChan) //关闭
}


//read data
func readData(intChan chan int, exitChan chan bool) {

	for {
		v, ok := <-intChan 
		if !ok {
			break
		}
		//time.Sleep(time.Second) 
		fmt.Printf("readData  读到数据=%v\n", v)
	}
	//readData 读取完数据后,即任务完成
	//exitChan<- true 
	//close(exitChan)
}

func main(){
	//创建俩个管道
	intChan := make(chan int, 50)
	exitChan := make(chan bool,1)

	go writeData(intChan)
	go readData(intChan,exitChan)

	//time.Sleep(time.Second * 10) 
	for {
		_,ok := <-exitChan
		if !ok {
			break
		}
	}
}

应用实例 2-阻塞

package main 
import (
	"fmt"
	"time"
)


//向 intChan 放入 1-8000 个数
func putNum(intChan chan int) {
	for i := 1; i <= 8000; i++ { 
		intChan<- i
	}
	//关闭 intChan 
	close(intChan)
}


//  从 intChan 取出数据,并判断是否为素数,如果是,就
//	//放入到 primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
	//使用 for 循环
	// var num int 
	var flag bool //
	for {
			time.Sleep(time.Millisecond * 10) 
			num, ok := <-intChan
		
			if !ok { 
			//intChan 取不到.. 
			break
			}
			flag = true //假设是素数
			//判断 num 是不是素数
			for i := 2; i < num; i++ {
				if num % i == 0 {//说明该 num 不是素数
				flag = false 
				break
			}
		}
	
		if flag {
			//将这个数就放入到 primeChan 
			primeChan<- num
		}
	}
	fmt.Println("有一个 primeNum 协程因为取不到数据,退出")
	//这里我们还不能关闭 primeChan
	//向 exitChan 写入 true 
	exitChan<- true
}


func main() {
	intChan := make(chan int , 1000)
	primeChan := make(chan int, 2000)//放入结果
	//标识退出的管道
	exitChan := make(chan bool, 4) // 4 个
	
	//开启一个协程,向 intChan 放入 1-8000 个数
	go putNum(intChan)
	//开启 4 个协程,从 intChan 取出数据,并判断是否为素数,如果是,就
	//放入到 primeChan 
	for i := 0; i < 4; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}
	
	//这里我们主线程,进行处理
	//直接
	go func(){
		for i := 0; i < 4; i++ {
		<-exitChan
		}
		//当我们从 exitChan 取出了 4 个结果,就可以放心的关闭 prprimeChan 
		close(primeChan)
	}()
	
	//遍历我们的 primeChan ,把结果取出 
	for {
		res, ok := <-primeChan 
		if !ok{
			break
		}
		//将结果输出 fmt.Printf("素数=%d\n", res)
	}
	
	fmt.Println("main 线程退出")
}

结论:使用 go 协程后,执行的速度,比普通方法提高至少 4 倍

channel 使用细节和注意事项

  1. channel 可以声明为只读,或者只写性质 【案例演示】
  2. channel 只读和只写的最佳实践案例
  3. 使用 select 可以解决从管道取数据的阻塞问题
package main 
import (
	"fmt"
	"time"
)


func main() {
	//使用 select 可以解决从管道取数据的阻塞问题
	//1.定义一个管道 10 个数据 i
	nt 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.Sprintf("%d", i)
	}
	
	//传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
	//问题,在实际开发中,可能我们不好确定什么关闭该管道.
	//可以使用 select 方式可以解决
	//label: 
	for {
		select {
			//注意: 这里,如果 intChan 一直没有关闭,不会一直阻塞而 deadlock
			//会自动到下一个 case 匹配
			case v := <-intChan :
				fmt.Printf("从 intChan 读取的数据%d\n", v) 
				time.Sleep(time.Second)
			case v := <-stringChan :
				fmt.Printf("从 stringChan 读取的数据%s\n", v) 
				time.Sleep(time.Second)
			default :
				fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n") 			
				time.Sleep(time.Second)
		return
		//break label
		}
	}
}
  1. goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
package main 
import (
	"fmt"
	"time"
)


//函数
func sayHello() {
	for i := 0; i < 10; i++ { 
	time.Sleep(time.Second) 
	fmt.Println("hello,world")
	}
}

//函数
func test() {
	//这里我们可以使用 defer + recover 
	defer func() {
		//捕获 test 抛出的 panic
		if err := recover(); err != nil { 
			fmt.Println("test()  发生错误", err)
		}
	}()
	//定义了一个 map
	var myMap map[int]string 
	myMap[0] = "golang" //error
}


func main() {
	go sayHello()
	go test()
	
	for i := 0; i < 10; i++ { 
		fmt.Println("main() ok=", i) 
		time.Sleep(time.Second)
	}
}