关于go:Go语言单向channel的争议

背景

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 main

import (
    "fmt"
)

// send-only channel
func testSendChan(c chan<- int) {
    c <- 20
}

// receive-only channel
func 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 channel
func testSendChan(c chan<- int) {
    c <- 20
}

// receive-only channel
func 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 main

import "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 adds
complexity to the spec that seems unnecessary.
What bugs or problems does it avoid to make this
special exclusion in the rules for make?
Russ

举荐浏览

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

总结

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

    channel nil 空的 非空非满 满了
    往channel发送数据 阻塞 发送胜利 发送胜利 阻塞
    从channel接收数据 阻塞 阻塞 接管胜利 接管胜利
    敞开channel panic 敞开胜利 敞开胜利 敞开胜利
  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/…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理