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

7次阅读

共计 4027 个字符,预计需要花费 11 分钟才能阅读完成。

背景

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/…
正文完
 0