深入 Golang channel (一)基础用法都在这了

1. 前言

你好哇!本文是「Golang 并发编程」系列的第 2 篇文章~

现在感觉这个坑开得有点大,没个一年半载的讲不清楚了……

go

本文将介绍 channel 相关的概念、语法和规则,不涉及原理和源码分析,更深入的内容,后面的更新会覆盖到,敬请期待~

2. Channel 简介

gomutex

关于并发编程,Rob Pike 有个名言:

不要通过共享内存来通信,要通过通信来共享内存
Don't (let computations) communicate by sharing memory, (let them) share memory by communicating (through channels)

在 go 语言中,channel 就是 goroutine 之间通过通信来共享内存的手段。可以把 channel 看作 go 程序内部的一个 FIFO (first in, first out) 队列,一些 goroutine 向其中生产数据,另外一些消费数据。

mutexwaitgroupsyncatomic

3. Channel 的类型和值

跟 slice 、map 这些内置类型一样,channel 作为一种元素类型,也是有具体的类型的,channel 只能传递声明的类型的值。

基础类型:双向与单向 channel

T
chan Tchan<- Tchan<-chan Tchan
func foo(ch1 <-chan int)  // 只能从 ch1 里读

func bar(ch2 chan<- int)  // 只能往 ch2 里写
复制代码
chan Tchan<- T<-chan Tchan<- T<-chan T

单向 channel 是一种函数传参时的安全性约束,在实际使用中几乎不可能单独去声明一个单向的 channel。

buffered channel + unbuffered channel + nil channel

每个 channel 类型的值都会有一个容量(capacity),根据 capacity 大小来区分,可以分为两种:

  • buffered channel:带缓冲的 channel,cap > 0
  • unbuffered channel:不带缓冲的 channel,cap = 0

使用 make 创建 channel:

ch1 := make(chan int, 10)  // buffered channel, cap = 10
ch2 := make(chan int) // unbuffered channel, cap = 0 (make chan 函数第二参数默认值为 0)
var ch3 chan int  // nil 是 chan 的零值(zero value)
复制代码
nil channelmake

4. channel 的 7 种操作

1. 向 channel 发送值

ch <- v
复制代码

需要注意:

vch<-channel-sendch

2. 从 channel 里读取结果

<- ch
复制代码
channel-receivechannel-sendchchannel-receive
v = <-ch
v, sentBeforeClosed = <-ch  // 先关闭再发送 v,则返回 false
复制代码
ok-idiomclose(ch)
package main

func main() {
    done := make(chan struct{})
    c := make(chan int)

    go func() {
        defer close(done)
        
        for {
            x, ok := <-c
            
            if !ok {	// close 时会收到一条消息,x 值为 0,ok 为 false
                return
            }

            println(x)
        }
    }()

    c <- 1
    c <- 2
    c <- 3
    close(c)
    <-done	// close 时会收到消息,解除阻塞
}
复制代码

3. for-range 操作

for-rangeok-idiomfor-range
func main() {
    done := make(chan struct{})
    c := make(chan int)

    go func() {
        defer close(done)
        
        for x := range c {
            println(x)
        }
    }()

    c <- 1
    c <- 2
    c <- 3
    close(c)
    <-done	// close 时会收到消息,解除阻塞
}
复制代码

4. select 多路选择

将在下文专门讨论

5. 关闭 channel

close(ch)
复制代码
close

错误的 close 会引发程序的 panic,关于如何优雅关闭 channel,我会在后面的「反面教材:panic 和内存泄漏」主题里展开,敬请期待~继续挖坑……

6. 返回 channel 的容量(capacity)

cap(ch)
复制代码
capint

7. 返回 channel buffer 中值的数量

len(ch)
复制代码
caplencaplen

5. 阻塞场景梳理

针对根据 channel 是否为空和是否关闭,可以分成以下三类来讨论:

  1. 空 channel (nil channel)
  2. 非空已关闭 channel
  3. 非空未关闭 channel
操作为空非空已关闭非空未关闭
closepanicpanic成功 close
写入永久阻塞panic成功写入或阻塞
读取永久阻塞永不阻塞成功读取或阻塞

要理解这几种现象,就要看下 channel 的内部结构了,可以认为 channel 内部有三个 FIFO 队列

  1. 接收数据的 goroutine 队列,是一个无限长的链表,这个队列里的 goroutine 都处于阻塞状态,等待数据从 channel 写入
  2. 发送数据的 goroutine 队列,也是一个无限长的链表,这个队列里的 goroutine 都处于阻塞状态,等待数据向 channel 写入。每个 goroutine 尝试发送的值也和 goroutine 一起存在这个队列里
  3. 值 buffer 队列,是一个环形队列(ringbuffer),它的大小跟 channel 的容量相同。存在这个 buffer 队列里的值跟 channel 元素的类型相同。如果当前 buffer 队列里储存的值的数量达到了 channel 的容量,这个 channel 就「满了」,对于 unbuffered channel 而言,它总是既在「空」状态,又在「满」状态。

更多的原理性的分析,将在本系列的后续「原理与源码分析」文章展开。

6. 多路选择操作符 select

switch-caseare-you-ready-polling

阻塞与非阻塞 select

select 默认是阻塞的,当没有 case 处于激活状态时,会一直阻塞住,极端的甚至可以这样用...

select {
    // 啥也不干,一直阻塞住
}
复制代码
default
select {
    case x, ok := <-ch1:
        ...
    case ch2 <- y:
        ...
    default:
        fmt.Println("default")

}
复制代码

多 case 与 default 执行的顺序

整体流程如图所示: 需要注意:

switch-caseselect-case
func main() {
	  var ch chan int
	  i := 0
	  for {
		  select {
		  case <-ch:  // nil channel 永远阻塞
			    fmt.Println("never...")
		  default:
			    fmt.Printf("in default, i = %d\n", i)
		  }
      i++
	  }
}
复制代码

小结

<-ok-idiomselect-caseclosefor-range

如果本文对你有帮助,记得「关注」、「点赞」、「在看」走起,也欢迎留言讨论,你的反馈是我更新的动力~

To be continued...

参考资料

  • 雨痕 -《Go 语言学习笔记》第 8 章,并发