背景

Go内置的数据结构channel置信大家都很相熟,channel在Go语言里表演了十分重要的角色。

  • channel能够帮忙实现goroutine之间的通信和同步。
  • Go语言通过goroutine和channel实现了CSP(Communicating Sequencial Process)模型。

Go语言的发明人之一Robe Pike说过上面这句话:

Don't communicate by sharing memory, share memory by communicating.

这句话里暗藏的含意其实是心愿Go开发者应用channel来实现goroutine之间的通信。

channel分为2种类型:

  • bi-directional channel(双向channel):既能够往channel里发送数据,也能够从channel接收数据
  • uni-directional channel(单向channel)

    • send-only channel:只能往channel发送数据,不能从channel接收数据,否则会编译报错
    • receive-only channel:只能从channel接收数据,不能往channel发送数据,否则会编译报错

单向channel的一个典型应用场景是作为函数或办法参数,用来管制只能往channel发送数据或者只能从channel接收数据,防止误操作。

package mainimport (    "fmt")// send-only channelfunc testSendChan(c chan<- int) {    c <- 20}// receive-only channelfunc testRecvChan(c <-chan int) {    result := <-c    fmt.Println("result:", result)}func main() {    ch := make(chan int, 3)    testSendChan(ch)    testRecvChan(ch)}

比方下面这段程序,testSendChan的参数是一个send-only channel,testRecvChan的参数是一个receive-only channel。

实参ch是一个双向channel,Go runtime会把双向channel转换成单向channel传给对应的函数。

问题

下面的例子是单向channel作为函数形参,双向channel作为函数实参,make函数创立的是一个双向channel。

那咱们能够用make来创立单向channel么?make创立的单向channel有理论应用场景么?有潜在的坑么?

咱们先看看如下2道题目:

题目1

ch := make(<-chan int)close(ch)fmt.Println("ok")
  • A: 打印ok
  • B: 运行时报错:fatal error - deadlock
  • C: 运行时报错:painic
  • D: 编译失败

题目2

c := make(chan<- int)close(c)fmt.Println("ok")
  • A: 打印ok
  • B: 运行时报错:fatal error - deadlock
  • C: 运行时报错:painic
  • D: 编译失败

大家能够先略微进展一下,想想这2道题的答案会是什么。

解析

答案

  • 题目1的答案是D,题目1创立了一个receive-only channel,只能从channel里接管值,不能往channel里发送值。

    对于receive-only channel不能close,如果做close操作,会有如下报错:

./main.go:9:7: invalid operation: close(ch) (cannot close receive-only channel)
  • 题目2的答案是A,题目2创立了一个send-only channel,只能往channel里发送值,不能从channel里接管值。

    对于send-only channel能够失常close。

为什么receive-only channel不能close,然而send-only channel能够close呢?

这是因为receive-only channel示意只能从这个channel里接收数据,应用方对这个channel只有读权限,对channel没有写权限,即不能往channel发送数据或者对channel做close操作。

所以题目1的答案是D,题目2的答案是A。

衍生问题

// send-only channelfunc testSendChan(c chan<- int) {    c <- 20}// receive-only channelfunc testRecvChan(c <-chan int) {    result := <-c    fmt.Println("result:", result)}func main() {    ch := make(chan int, 3)    testSendChan(ch)    testRecvChan(ch)}

下面这段代码是单向channel的典型应用场景,函数实参是双向channel,函数参数是单向channel,咱们能够通过send-only channel往channel发送数据,通过receive-only channel从channel里接收数据,这个很好了解。然而要留神的是这里的函数实参是双向channel

仔细的开发者可能有个疑难,对于下面的2道题目,make创立的是单向channel,要么只能往这个channel里发数据,要么只能从这个channel接收数据,而不像下面代码里make创立的channel是个双向channel。

问题来了,对于make创立的单向channel,有理论用处么

比方make(chan<- int)创立的send-only channel,只能往这个channel发数据,没方法从这个channel里读数据,有啥用么?

比方make(<-chan int)创立的receive-only channel,只能从这个channel里收数据,没方法往这个channel里发数据,有啥用么?

对于这个问题,其实Go语言的大佬之间也产生过比拟大的争议。

Golang团队的Brad Fitzpatrick就埋怨过,认为编译器应该间接对make创立单向channel报错,因为make创立的单向channel没啥用。

I'm surprised the compiler permits create send- or receive-only channels:------package mainimport "fmt"func main() {    c := make(<-chan int)    fmt.Printf("%T\n", c)}------Is that an accident?I can't see what utility they'd have.

Go语言负责人Russ Cox给的反馈是:认为没有明确理由让编译器禁止make创立单向channel,因为对单向channel如果有非法的操作,编译器还是会报错,而且让编译器间接禁止make创立单向channel会减少Go设计的复杂性,没有必要,得失相当。

I can't see a reason to disallow it either though.All I am saying is that explicitly disallowing it addscomplexity to the spec that seems unnecessary.What bugs or problems does it avoid to make thisspecial exclusion in the rules for make?Russ

举荐浏览

  • Go Quiz: 从Go面试题看channel的注意事项
  • Go Quiz: 从Go面试题看channel在select场景下的注意事项

总结

  1. 对于无缓冲区的channel,往channel发送数据和从channel接收数据都会阻塞。
  2. 对于nil channel和有缓冲区的channel,收发数据的机制如下表所示:

    channelnil空的非空非满满了
    往channel发送数据阻塞发送胜利发送胜利阻塞
    从channel接收数据阻塞阻塞接管胜利接管胜利
    敞开channelpanic敞开胜利敞开胜利敞开胜利
  3. channel被敞开后:

    • 往被敞开的channel发送数据会触发panic。
    • 从被敞开的channel接收数据,会先读完channel里的数据。如果数据读完了,持续从channel读数据会拿到channel里存储的元素类型的零值。

      data, ok := <- c 

      对于下面的代码,如果channel c敞开了,持续从c里读数据,当c里还有数据时,data就是对应读到的值,ok的值是true。如果c的数据曾经读完了,那data就是零值,ok的值是false

    • channel被敞开后,如果再次敞开,会引发panic。
  4. select的运行机制如下:

    • 选取一个可执行不阻塞的case分支,如果多个case分支都不阻塞,会随机选一个case分支执行,和case分支在代码里写的程序没关系。
    • 如果所有case分支都阻塞,会进入default分支执行。
    • 如果没有default分支,那select会阻塞,直到有一个case分支不阻塞。

开源地址

文章和示例代码开源在GitHub: Go语言高级、中级和高级教程。

公众号:coding进阶。关注公众号能够获取最新Go面试题和技术栈。

集体网站:Jincheng's Blog。

知乎:无忌。

福利

我为大家整顿了一份后端开发学习材料礼包,蕴含编程语言入门到进阶常识(Go、C++、Python)、后端开发技术栈、面试题等。

关注公众号「coding进阶」,发送音讯 backend 支付材料礼包,这份材料会不定期更新,退出我感觉有价值的材料。还能够发送音讯「进群」,和同行一起交流学习,答疑解惑。

References

  • https://twitter.com/val_delep...
  • https://github.com/golang/go/...