通道(Channel)

通道是什么,为什么使用通道

「不要通过共享内存来通信,而应该通过通信来共享内存」

通道可以在多个 goroutine 之间传递数据

一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符 <-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向

如何初始化通道

1
2
3
4
5
6
7
8
func main() {
ch1 := make(chan int, 3)
ch1 <- 2
ch1 <- 1
ch1 <- 3
elem1 := <-ch1
fmt.Printf("The first element received from channel ch1: %v\n",elem1)
}
  1. 使用 make 函数声明并初始化通道
  2. 通道的容量是 int 类型,但是不能小于 0 (缓冲通道和非缓冲通道)

通道的发送和接收操作都有哪些基本的特性

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的
  2. 发送操作和接收操作中,对元素值的处理都是不可分割的
  3. 发送操作在完全完成之前会被阻塞,接收操作也是如此
  4. 元素值从外界进入通道时会被复制
    • 非缓冲通道的数据是直接从发送方复制到接收方
    • 大多数情况下缓冲通道都会中转数据,如果发送时正好有人在等着接收,则会直接复制过去

什么时候会阻塞

正常情况下

  • 缓冲通道:如果通道已满,所有的发送操作会被依次阻塞
  • 非缓冲通道:无论是发送操作还是接收操作,一开始执行的时候都会被阻塞,直到配对的操作也开始执行

不正常情况

  • 值为 nil (没有使用 make 初始化)时,两种操作都会被永久阻塞

什么时候会 panic

  • 对关闭的通道发送数据
  • 对关闭的通道再次关闭

关闭的通道有什么性质

关闭通道是指关闭通道的入口,通道关闭后仍可以从中取出数据

如果接收两个值,第二个值是 bool ,表示还能不能取出元素

如果值为 false ,表示通道已经关闭并且没有元素了,此时第一个元素会是零值

如果值为 true ,表示成功地取出了元素,注意你此时无法判断通道是否关闭

因此使用这个 bool 值判断通道是否关闭是有延迟的

永远在发送方关闭通道,不能在接收方关闭通道

什么是单向通道,有什么用

如果声明时包含接收操作符(<-),就是单向通道

  • 发送通道:chan<- ,只能发不能收
  • 接收通道:<-chan ,只能收不能发

单向通道可以限制其他代码的行为

1
2
3
func SendInt(ch chan<- int) {
ch <- rand.Intn(1000)
}

这里限制了函数内只能向通道内发送元素

1
2
3
type Notifier interface {
SendInt(ch chan<- int)
}

类似的,还有可以用在接口里

1
2
3
4
5
6
7
8
9
func getIntChan() <-chan int {
num := 5
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
return ch
}

这里限制了得到返回值的程序,只能从通道中接收元素

怎么用 for range 从通道中取出元素

1
2
3
4
intChan2 := getIntChan()
for elem := range intChan2 {
fmt.Printf("The element in intChan2: %v\n", elem)
}

性质与正常操作时相同

select 如何与通道连用

1
2
3
4
5
6
7
8
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
default:
fmt.Println("没有收到任何消息")
}

select 语句只能与通道联用,是一种多路通信选择的控制结构,允许一个 goroutine 等待多个通信操作

它由若干个分支组成。每次执行这种语句的时候,只有一个分支中的接收/发送代码会被运行

select` 语句的分支选择规则有哪些

  • 如果所有分支都阻塞,则会运行 defult 分支

    (也就是说含有默认分支的 select 永远不会阻塞)

  • 如果所有分支都阻塞,又没有 defult 分支,则会一直阻塞,直到有分支可以执行

  • 如果同时有多个分支可以执行,则会随机选择一个

使用 select 的注意事项

  • 如果通道关闭了,接收并不会阻塞,而同时获得零值与 false

    所以如果发现通道关闭了,应当及时屏蔽对应的分支或者采取其他措施

    (将通道赋值为 nil 可以屏蔽改对应的分支了,因为 nil 的通道是一直阻塞的)

  • select 语句只会将某分支的通道操作运行一次,所以如果你想连续操作的话,可以在 for 中使用 select

    但是如果你在 select 中使用 break 的话,只会跳出当前 select,而 for 并不会跳出

    (如果想将 for 也跳出,可以在 for 前面放一个 label,再将 labelbreak 一起使用)

  • 尽管 select 本身是并发安全的,但是不代表你的 case 表达式和分支中的代码也是并发安全的