10.1 通道与 CSP 的工程化
本节内容提供一个线上演讲:YouTube 在线, Google Slides 讲稿。
CSP 给了 Go 一个主张:进程之间不共享状态,只靠传递消息来协调(1.3)。 这一节关心的是另一个问题:一门工程语言要把这个主张落地,需要把「通信」做成什么样子, 才能让普通程序员顺手用对。Go 的答案是 channel,一个一等的同步兼通信原语。本节先把它在 语言表层的模型讲清楚,它的类型、收发语法、有无缓冲的两种语义、方向限定与 nil 行为, 为后续各节深入运行时实现(10.2–10.7)铺好直觉。
10.1.1 把通信做成一等原语
「不要以共享内存的方式通信,而要以通信的方式共享内存。」这句格言常被引用,但它在 Go 里不是 一句口号,而是由 channel 这个具体的语言构造兑现的。CSP 的渊源、它与 1978 年原始论文的异同, 已在 1.3 交代,这里不再重复,只接着说 Go 在工程上做了 什么取舍。
把 CSP 的「通信」搬进一门通用语言,至少要回答三件事:通信经由什么载体、这个载体能否像普通值
一样使用、它如何与类型系统咬合。Go 给出的载体是 channel,并且把它做成了一等(first-class)
的:channel 是有类型的值,可以由 make 创建、用变量持有、作为参数传给函数、作为返回值传出、
存进结构体字段、放进 slice 或 map。这一点与 Hoare 原始 CSP 按进程名直接通信的设计不同
(1.3.2),也正是 Go 把抽象主张变为可组合工具的关键:
既然 channel 是值,「把一个通信端口交给另一段代码」就退化成了普通的传参,无需任何特殊机制。
channel 同时承担两件常被分开的事:同步与数据传递。一次收发既搬运了一个值,又在收发 双方之间建立了一个发生序(happens-before)关系,后者保证了「发送方在发送前写下的内存,接收方 在接收后一定看得见」。换句话说,channel 不仅是一根传值的管子,它还是同步原语。这条发生序 保证是 channel 能替代显式加锁的根基,其精确条文留待 10.6 与 11.9 展开,本节只需记住:收发自带同步。
10.1.2 表层模型:从 make 到 select
先用一组最小的代码把 channel 的表层 API 过一遍,建立操作直觉,细节在后续小节展开。
创建用 make。无缓冲与有缓冲只差一个容量参数:
| |
发送与接收都用箭头算符 <-,箭头指向数据流动的方向:
| |
接收还有一个双返回值形式,第二个布尔值 ok 用来区分「收到一个真实的值」与「channel 已关闭
且缓冲已空,收到的是零值」:
| |
close 关闭一个 channel,宣告不会再有发送。关闭之后,已在缓冲中的值仍可被接收,缓冲取空后
继续接收则立即返回零值与 ok == false。关闭的完整语义(谁该关闭、向已关闭的 channel 发送或
重复关闭会 panic)留到 10.4,这里只取其表层行为。
range 在 channel 上迭代,反复接收直到 channel 关闭并取空,是消费一条数据流的惯用写法:
| |
select 在多路通信间择一而行。它同时盯住若干收发操作,挑一个就绪的执行;若多个同时就绪,
随机选一个;若都不就绪,则阻塞,除非写了 default 分支:
| |
select 是把 CSP 的「守卫与选择指令」工程化的产物,它的随机择一、公平性与两轮加锁实现是
10.5 的主题。至此,channel 的全部表层算符就是这几样:make、<-、close、
range、select。接下来的几小节,逐一把其中容易用错的语义点讲透。
10.1.3 无缓冲与有缓冲:会合还是排队
容量参数把 channel 分成语义迥异的两类,理解这条分界是用对 channel 的前提。
无缓冲 channel(容量 0)是一次会合(rendezvous)。发送方执行 ch <- v 时,若此刻
没有接收方在等,发送方就地阻塞,直到某个接收方执行 <-ch 与之配对;配对成功的那一刻,值
直接从发送方交到接收方,两者才各自继续。接收先到也对称地阻塞等发送。于是无缓冲 channel 的
一次成功收发,意味着收发双方在那一刻确实碰过头:发送返回时,你确知值已被某个接收方收走。
| |
有缓冲 channel(容量 $n>0$)是一个容量为 $n$ 的有界队列。发送方执行 ch <- v 时,
只要队列未满就把值放进队列、立即返回,不必等接收方到场;队列满了才阻塞。接收方从队头取值,
队列空了才阻塞。这里的关键差别在于:有缓冲发送返回时,值可能还躺在队列里,接收方尚未取走。
你换来了发送与接收在时间上的解耦,代价是失去了「发送返回即对方已收到」这条会合保证。
flowchart LR
subgraph U["无缓冲:会合"]
S1["发送方 ch<-v"] -->|"双方就位才成交<br/>值直接交付"| R1["接收方 <-ch"]
end
subgraph B["有缓冲 n:有界队列"]
S2["发送方 ch<-v"] -->|未满则入队| Q["队列 0..n-1"]
Q -->|非空则出队| R2["接收方 <-ch"]
end何时用哪种,可以归到几条朴素的判断上。需要「我做完、对方确认收到」这类强同步信号时,用无缓冲,
它的会合语义恰好就是握手。需要让生产者与消费者在节奏上解耦、削平突发,或想给在途数据设一个
明确的上限时,用有缓冲,队列容量就是这个上限。容量还能当一把信号量用:make(chan struct{}, k)
配合「发送占位、接收释放」,就限制了同时进行的并发数不超过 $k$(10.7)。
需要警惕的是一种常见的误解,把缓冲当作免费的提速旋钮。缓冲解耦的是时机,不是计算本身; 它不会让总工作量变少,反而引入了新的问题:缓冲多大才合适、满了之后的背压如何处理、在途数据 在崩溃时如何交代。一个拍脑袋设成很大的缓冲,往往只是把「何时阻塞」推后,并把队列堆积的风险 藏了起来。容量是一个需要论证的设计参数,不是默认就该往大里调的性能开关。
10.1.4 方向限定:让类型替你守约
channel 类型可以带方向,把一个双向 channel 收窄为只发或只收:
| |
方向写在函数签名里,是一种廉价而有力的 API 卫生手段。考虑一个生产者函数,它只该往 channel 里写、绝不该读,把参数声明成只发,编译器就替你拦住任何误读:
| |
双向 channel 可以隐式赋给单向类型,反之不行。于是惯常的写法是:在一处用 make 造出双向
channel,再把它分别以只发、只收两种受限视图传给生产者与消费者。方向不改变运行时行为,它纯粹
是编译期的约束,把「这一端只该发」「那一端只该收」的意图写进类型,让违约在编译时而非运行时
暴露。这也顺手把「谁负责 close」这件容易出错的事钉在了类型上:只有持有发送端的一方才有资格
调用 close,因为对只收视图调用 close 根本无法通过编译。
10.1.5 nil channel:永久阻塞的妙用
一个尚未 make、值为 nil 的 channel,对它收或发都会永久阻塞:
| |
孤立地看,这像是一个只会制造死锁的陷阱。但放进 select,它就成了一个干净的开关。select
会忽略那些永远不会就绪的分支,而一个 nil channel 的分支正是永远不就绪。于是「把某个 case 的
channel 置为 nil」就等于在运行时动态关掉这个分支,无需改动 select 的结构。
这个技巧在「读完一路输入后不再监听它」的场景里尤其顺手。下面的循环同时消费两路输入,某一路
关闭后,就把对应的 channel 变量置为 nil,从此 select 不再选中那条已耗尽的分支,避免了对
已关闭 channel 反复收到零值的忙转:
| |
把 nil channel 的「永久阻塞」与 select 的「忽略不就绪分支」两条规则合起来,就得到了一个
表达力很强的惯用法。这也提示我们,channel 的几条表层规则并非孤立,它们彼此组合才构成真正
好用的工具,后续讲实现时会反复见到运行时如何精确兑现这些组合。
10.1.6 本章的路线
有了表层模型,接下来逐层深入 channel 在运行时里的实现。各节的安排如下:
- 10.2 hchan:通道的内部结构:一个 channel 在内存里长什么样,环形缓冲、 收发等待队列与那把锁。
- 10.3 收发与直接传递:一次收发如何在两个 goroutine 间会合,以及绕过缓冲 把数据直接写入对方栈的优化。
- 10.4 关闭的语义:
close如何向所有等待者广播,以及那几条会 panic 的边界。 - 10.5 select 的实现:
selectgo如何在多路通信间随机、公平、无死锁地择一。 - 10.6 内存模型与无锁演进:收发建立的发生序保证,以及 channel 为何选择 有锁、又能否更进一步。
- 10.7 工程实践与跨语言对照:常见的 channel 模式与陷阱,以及与别家并发原语 的对照。
读完本节,读者应已能写出正确的 channel 代码;读完本章,则能讲清这些代码为何如此运转。
延伸阅读的文献
- The Go Authors. The Go Programming Language Specification: Channel types, Send statements, Receive operator, Close. https://go.dev/ref/spec#Channel_types
- The Go Authors. Effective Go: Channels. https://go.dev/doc/effective_go#channels
- Rob Pike. Go Concurrency Patterns. Google I/O 2012. https://go.dev/talks/2012/concurrency.slide
- Sameer Ajmani. Advanced Go Concurrency Patterns. Google I/O 2013. https://go.dev/talks/2013/advconc.slide (nil channel 在 select 中关闭分支的技巧)
- C. A. R. Hoare. “Communicating Sequential Processes.” Communications of the ACM, 21(8), 1978. https://doi.org/10.1145/359576.359585
- 本书 1.3 顺序进程通讯、 10.6 内存模型与无锁演进、11.9 内存一致模型.
许可
© 2018-2026 The golang.design Initiative Authors. Licensed under CC-BY-NC-ND 4.0.