缓冲的Channels

缓冲:缓冲 channel 类似一个有容量的队列。当队列满的时候发送者会阻塞;当队列空的时候接收者会阻塞。

缓冲的Channels - 图1

此外在使用channel之后可以进行关闭,关闭channel后,对该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel之行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话讲产生一个零值的数据。

使用内置的close函数就可以关闭一个channel:

  1. close(ch)

但是关于关闭channel 有几点需要注意:

  • 重复关闭 channel 会导致 panic.
  • 向关闭的 channel 发送数据会 panic.
  • 从关闭的 channel 读数据不会 panic,读出channel中已有的数据之后再读就是channel类似的默认值,比如 chan int 类型的channel关闭之后读取到的值为 0.

这里我们需要区分一下第三种channel 中的值是默认值还是channel 关闭了。可以使用 ok-idiom 方式,这种方式在 map 中比较常用.

  1. ch := make(chan int, 10)
  2. ...
  3. close(ch)
  4. // ok-idiom
  5. val, ok := <-ch
  6. if ok == false {
  7. // channel closed
  8. }

Channel的典型用法

  • goroutine 使用channel通信

    1. func main() {
    2. x := make(chan int)
    3. go func() {
    4. x <- 1
    5. }()
    6. <-x
    7. }
  • select

select语句选择一组可能的send操作和receive操作去处理。它类似switch,但是只是用来处理通讯(communication)操作。 它的case可以是send语句,也可以是receive语句,亦或者default。

receive语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。

select在一定程度上可以类比于linux中的 IO 多路复用中的 select。后者相当于提供了对多个 IO 事件的统一管理,而 Golang 中的 select 相当于提供了对多个 channel 的统一管理。当然这只是 select 在 channel 上的一种使用方法。

  1. select {
  2. case e, ok := <-ch1:
  3. ...
  4. case e, ok := <-ch2:
  5. ...
  6. default:
  7. }

这里需要注意的是 select 中的 break 只能跳到 select 这一层。select 使用的时候一般需要配合 for 循环使用,因为正常 select 里面的流程也就执行一遍。这么来看 select 中的 break 就稍显鸡肋了。所以使用 break 的时候一般配置 label 使用,label 定义在 for循环这一层。

  1. for {
  2. select {
  3. ...
  4. }
  5. }
  • range channel

使用range channel 我们可以直接取到 channel 中的值。当我们使用 range 来操作 channel 的时候,一旦 channel 关闭,channel 内部数据读完之后循环自动结束。

  1. func consumer(ch chan int) {
  2. for x := range ch {
  3. fmt.Println(x)
  4. ...
  5. }
  6. }
  7. func producer(ch chan int) {
  8. for _, v := range values {
  9. ch <- v
  10. }
  11. }
  • 超时控制

select有很重要的一个应用就是超时处理。由于如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况通常在很多操作情况下都需要超时控制,我们可以利用 select 实现超时控制:

  1. func main() {
  2. c1 := make(chan string, 1)
  3. go func() {
  4. time.Sleep(time.Second * 2)
  5. c1 <- "result 1"
  6. }()
  7. select {
  8. case res := <-c1:
  9. fmt.Println(res)
  10. case <-time.After(time.Second * 1):
  11. fmt.Println("timeout 1")
  12. }
  13. }

这里利用的是time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。

  • channel同步

channel可以用在goroutine之间的同步。 下面的例子main中goroutine通过done channel等待 mission完成任务。 mission做完任务后只需往channel发送一个数据就可以通知main goroutine任务完成。

  1. func mission(done chan bool) {
  2. time.Sleep(time.Second)
  3. // 通知任务已完成
  4. done <- true
  5. }
  6. func main() {
  7. done := make(chan bool, 1)
  8. go mission(done)
  9. // 等待任务完成
  10. <-done
  11. }

在Golang中的channel 将goroutine 隔离开,并发编程的时候可以将注意力放在 channel 上。在一定程度上,这个和消息队列的解耦功能还是挺像的。