Golang管道(channel)及其应用
管道(channel)
全局变量加锁
解决程序同步问题
package main import ( "fmt" "sync" "time" ) //需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。 //最后显示出来。要求使用goroutine完成 //思路: //1,编写一个函数,来计算各个数的阶乘,并放入到map中. //2.我们启动的协程多个,统计的将结果放入到 map 中 //3. map 应该做出一个全局的 var ( myMap = make(map[int]int, 10) //声明全局互斥锁 //lock 是一个全局的互斥锁 //sync 是包:synchronized //Mutex 是互斥 lock sync.Mutex ) func test(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } //加锁 lock.Lock() myMap[n] = res //解锁 lock.Unlock() } func main() { //开启多个协程完成任务 for i := 1; i <= 20; i++ { go test(i) } //休眠 time.Sleep(time.Second * 10) lock.Lock() //遍历结果 for i, v := range myMap { fmt.Printf("map[%d] = %d \n", i, v) } lock.Unlock() }
前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,仅仅是估算。
如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
上面种种分析都在呼唤一个新的通讯机制-channel
channel介绍
channle本质就是一个数据结构-队列
数据是先进先出【FIFO : first in first out】
线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
channel时有类型的,一个string的channel只能存放string类型数据。
package main import "fmt" func main() { //1.创建一个可以存放3个int类型的管道 var intChan chan int intChan = make(chan int, 3) //intChan 是什么? fmt.Printf("intChan的值=%v intChan本身的地址=%v\n", intChan, &intChan) //3.向管道写入数据 intChan <- 10 num := 200 intChan <- num intChan <- 100 //注意: //写入数据时,不能超过容量 //4.看看管道的长度和容量 fmt.Printf("chanel len = %v cap = %v\n", len(intChan), cap(intChan)) //5.从管道中去读数据 var num2 int num2 = <-intChan fmt.Println("num2=", num2) fmt.Printf("chanel len = %v cap = %v\n", len(intChan), cap(intChan)) //6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock }
注意事项
channel中只能存放指定的数据类型
channle的数据放满后,就不能再放入了
如果从channel取出数据后,可以继续放入
在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
package main import "fmt" type Cat struct { Name string Age int } func main() { //定义一个存放任意数据类型的管道 allChan := make(chan interface{}, 3) allChan <- 10 allChan <- "tom" cat := Cat{"描", 10} allChan <- cat //我们希望获得管道中的第三个元素,则将前两个推出 <-allChan <-allChan newCat := <-allChan //从管道中取出的Cat是什么? fmt.Printf("newCat=%T, newCat= %v \n", newCat, newCat) a := newCat.(Cat) //类型断言 fmt.Printf("newCat.Name=%v\n", a.Name) }
channel的遍历和关闭
管道的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据。
package main import "fmt" func main() { intChan := make(chan int, 3) intChan <- 300 intChan <- 200 close(intChan) //这时不能写入数据到chanel //管道关闭,读取数据是可以的 n1 := <-intChan fmt.Println(n1) }
管道的遍历
channel支持for-rarge的方式进行遍历,请注意两个细节
1)在遍历时,如果channel没有关闭,则回出现deadlock的错误
2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
//遍历管道 intChan2 := make(chan int, 100) for i := 0; i < 100; i++ { intChan2 <- i * 2 //放入一百个数据 } //在遍历时,如果channel没有关闭,则会出现deadlock的错误 //关闭管道 close(intChan2) //遍历管道不能使用for 循环 for v := range intChan2 { fmt.Println("v=", v) }
goroutine和channel结合案例1
package main import ( "fmt" ) //write data func writeData(intChan chan int) { for i := 1; i <= 50; i++ { //放入数据 intChan <- i fmt.Println("writeDate ", i) } close(intChan) } //read data func readData(intChan chan int, exitChan chan bool) { for { v, ok := <-intChan if !ok { break } 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) for { _, ok := <-exitChan if !ok { break } } }
管道阻塞
如果只是向管道写入数据,而没有读取,就会出现阻塞而dead lock
goroutine和channel结合案例2 package main import "fmt" //想 intChan 放入 1-8000个数 func putNum(intChan chan int) { for i := 0; i < 8000; i++ { intChan <- i + 1 } //关闭 close(intChan) } func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { //使用for循环 //var num int var flag bool //标识是否为素数 for { num, ok := <-intChan if !ok { break } flag = true //判断num是不是素数 for i := 2; i < num; i++ { if num%i == 0 { flag = false break } } if flag { //将这个素数放入primeChan primeChan <- num } } fmt.Println("有一个primeNum 协程 因为取不到数据 ,退出") //向退出的管道写入true exitChan <- true } func main() { intChan := make(chan int, 1000) primeChan := make(chan int, 2000) //放入结果 //标识退出的管道 exitChan := make(chan bool, 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 } //关闭管道 close(primeChan) }() //遍历 primeNum for { res, ok := <-primeChan if !ok { break } fmt.Println("素数=", res) } fmt.Println("main退出") }
channel细节
channel只读、只写
package main import "fmt" func main() { //管道可以声明为只读或者只写 //1. 在默认情况下,管道都是双向 //var chan1 chan int //双向,可读可写 //2. 声明为只写 var chan2 chan<- int chan2 = make(chan int, 3) chan2 <- 20 fmt.Println("chan2 = ", chan2) //3.声明为只读 var chan3 <-chan int num2 := <-chan3 fmt.Println("num2", num2) }
使用select可以解决从管道取数据的阻塞问题
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 个数据 sting 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) case v := <-stringChan: fmt.Printf("从stringChan读取的数据 %s \n", v) default: fmt.Println("都去不到了...") //break label return } } }
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) } }() 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) } }