likes
comments
collection
share

协程之间通信——Channel

作者站长头像
站长
· 阅读数 18

在Go中,我们可以通过go关键字来开启一个goroutine,那有时候我们需要不同的goroutine之间能够通信,这时候就需要用到我们的管道——channel。

Channel

官方定义:

Channels are a typed conduit through which you can send and receive values with the channel operator Channel是一个可以收发数据的管道

初始化:

channel_name := make(chan channel_type)
channel_name := make(chan channel_type, size)  //创建带有缓存的管道,size为缓存大小

下面来看看具体的操作:

ch := make(chan int)         // 创建一个管道ch
ch <- v                      // 向管道ch中发送数据v.
v := <-ch                    // 从管道中读取数据存储到变量v
close(ch)                    // 关闭管道ch

这里需要注意close(ch)这个操作,管道用完了,需要对其进行关闭,避免程序一直在等待以及资源的浪费。但是关闭的管道,仍然可以从中接收数据,只是接收到的的数据永远是零值。

这样会出现一个问题,要是一个int类型的管道,无法区分读取到的是正确的零值还是数据已经读去完了而读取到的零值。

解决办法有以下两个:

  • 1. 判定读取
func main() {
   ch := make(chan int, 5)
   ch <- 1
   close(ch)
   go func() {
      for i := 0; i < 5; i++ {
         v,ok := <-ch     // 判断句式读取
         if ok{
            fmt.Printf("v=%d\n", v)
         }else{
            fmt.Printf("channel数据已读完\n")
         }
      }
   }()
   time.Sleep(2 * time.Second)
}
v=1
channel数据已读完
channel数据已读完
channel数据已读完
channel数据已读完

在读取channel数据的时候,用ok句式做了判断,当管道内还有数据能读取的时候,ok为true,当管道关闭后,ok为false。

  • 2.For range读取

在上面例子中,我们明确了读取的次数是5次,但是我们往往在更多的时候,是不明确读取次数的,只是在Channel的一段读取数据,有数据我们就读,直到另一段关闭了这个channel,这样就可以用for range这种优雅的方式来读取channel中的数据了

func main() {
   ch := make(chan int, 5)
   ch <- 1
   ch <- 2
   close(ch)
   go func() {
      for v := range ch {
         fmt.Printf("v=%d\n", v)
      }
   }()
   time.Sleep(2 * time.Second)
}
v=1
v=2

主goroutine往channel里写了两个数据1和2,然后关闭,子channel也只能读取到1和2。这里在主goroutine关闭了channel之后,子goroutine里的for range循环才会结束。

双向channel和单向channel

channel根据其功能又可以分为双向channel和单向channel,双向channel即可发送数据又可接收数据,单向channel要么只能发送数据,要么只能接收数据。下面分别是定义了单向读与单向写的代码。通过type定义了channel的类型

var ch = make(chan int)
type RChannel= <-chan int    // 定义类型
var rec RChannel = ch
var ch = make(chan int)
type SChannel = chan<- int  // 定义类型
var send SChannel = ch

解决的问题

Channel非常重要,Golang中有个重要思想:不以共享内存来通信,而以通信来共享内存。

说得更直接点,协程之间可以利用Channel来传递数据,如下的例子,可以看出父子协程如何通信的,父协程通过Channel拿到了子协程执行的结果。

func sum(s []int, c chan int) {
   sum := 0
   for _, v := range s {
      sum += v
   }
   c <- sum // send sum to c
}

func main() {
   s := []int{7, 2, 8, -9, 4, 0}

   c := make(chan int)
   go func() {
      sum(s[:len(s)/2], c)
      //time.Sleep(1 * time.Second)
   }()
   go sum(s[len(s)/2:], c)
   x, y := <-c, <-c // receive from c

   fmt.Println(x, y, x+y)
}
-5 17 12

Channel的几点总结

  • 关闭一个未初始化的 channel 会产生 panic
  • channel只能被关闭一次,对同一个channel重复关闭会产生 panic
  • 向一个已关闭的 channel 发送消息会产生 panic
  • 从一个已关闭的channel读取消息不会发生panic,会一直读取到零值
  • channel可以读端和写端都可有多个goroutine操作,在一端关闭channel的时候,该channel读端的所有goroutine 都会收到channel已关闭的消息
  • channel是并发安全的,多个goroutine同时读取channel中的数据,不会产生并发安全问题