Golang管道(channel)及其应用

作者: adm 分类: go 发布时间: 2022-08-30

管道(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)
   }
}

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!