想象你住的小区新装了一排快递柜。取件有完全不同的两种方式。第一种叫“面交”:快递员打电话约你,两人必须同时到场,一手交件一手签收,谁晚到谁就得站在原地等。第二种叫“快递柜”:快递员把包裹塞进格口拍个照就走,你什么时候有空什么时候来取,完全不用打照面;当然柜子满了快递员就得等,柜子空了你也取不到东西。 Go 语言的 channel(通道),就是 goroutine(Go 里的轻量级线程)之间的一套“快递系统”。无缓冲 channel 就像面交:发送方和接收方必须同时就绪,消息才能递出,否则发送方会被挂起干等。有缓冲 channel 则像带格口的快递柜:发送方看到有空位就放下消息离开,接收方没空时消息先在柜子里待一会儿。 很多 Go 初学者在这两种 channel 上栽跟头:要么往无缓冲 channel 里发消息却没人接收,程序直接死锁;要么给有缓冲 channel 设了个拍脑袋的容量,导致消息积压或内存浪费。搞懂这套“快递系统”的运作规则,你就能写出既流畅又不出死锁的并发程序。

岗位关联

有缓冲与无缓冲是 Go channel 面试最高频的两个考点,也是并发程序死锁的最常见根源。理解它们的同步与异步本质,是你写出健壮并发代码的关键一步。

目录
  1. 可视化演示
  2. 通信理念:快递柜代替黑板
  3. 无缓冲同步:面交的同步屏障
  4. 有缓冲异步:格口的异步缓冲
  5. 缓冲大小:格口数怎么定
  6. 死锁排查:谁在等谁
  7. 安全关闭:正确贴封条
  8. 单向约束:专用投递口
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示无缓冲channel同步通信、有缓冲channel异步解耦、死锁场景及channel关闭与len/cap监控机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

无缓冲 vs 有缓冲 无缓冲channel发送方阻塞直到接收方就绪,实现同步通信 有缓冲channel缓冲未满时不阻塞,实现异步解耦 Unbuffered: sync | Buffered: async with cap 缓冲大小决定生产消费速度差异容忍度

通信理念:快递柜代替黑板

小区以前收发件用一块公共黑板:谁有东西要交接,就在黑板上写一行字约时间。麻烦在于,两个快递员同时写字会把字迹糊在一起,于是只好先抢到粉笔、再加一把锁,等一个人写完另一个人才能写。日子久了大家发现,与其在黑板上互相打架,不如做一排快递柜:你把包裹放进格口,我从格口取走,彼此根本不碰同一块地方。

Go 的并发哲学正是如此。传统做法叫“通过共享内存来通信”:多个 goroutine 共享一个变量,用互斥锁保证同一时刻只有一个人能改它。而 Go 推荐“通过通信来共享内存”:用 channel 这条管道让数据从一方流到另一方,大家根本不共享变量。channel 是 goroutine 之间专门的通信管道,用 make 创建,参数是消息类型——make(chan int) 就是一条只传整数的管道。

发送A(ch,v):A→vch,接收B(ch):ch→vB\text{发送}_A(ch,v): A \xrightarrow{v} ch, \qquad \text{接收}_B(ch): ch \xrightarrow{v} B

1. 发送动作:A 把消息 v 投入通道 ch,消息进入管道的左端。

发送A(ch,v)\text{发送}_A(ch,v)

2. 接收动作:B 从通道 ch 取出消息 v,消息从管道右端流出。

接收B(ch)\text{接收}_B(ch)

3. 管道隔离:A 与 B 不直接连线,只各自面向通道,正是“通过通信共享内存”。

A↮B,A↔ch↔BA \not\leftrightarrow B, \quad A \leftrightarrow ch \leftrightarrow B

回到快递柜:发送方和接收方眼里只有格口,不需要知道对面是谁。channel 的价值,就是让并发程序各干各的,通过一条管道把数据递过去,谁都不用碰那块黑板。

无缓冲同步:面交的同步屏障

make(chan int) 不带容量参数时,得到的是无缓冲 channel。它像一场约好的面交:快递员和你必须同时出现在约定地点,快递员把包裹递到你手里的那一刻,交接才真正完成。任何一方先到,都要站在原地等对方。发送方执行 ch <- v,如果此刻没有接收方正在 <-ch 上等待,发送方就会被挂起,直到接收方出现,消息瞬间递出,两边同时恢复。

无缓冲 channel 的“无缓冲”不是容量为 0 存不下数据,而是它根本不提供存放数据的格子——数据从发送方手里直接递进接收方手里,中间没有中转站。这带来一个关键特性:同步。发送方的发送动作结束,意味着接收方已经接走消息,两者之间像隔了一道栅栏,前后的代码执行顺序有了明确的时间先后。

Ssend(v) 返回  ⟺  Rrecv 已就绪S_{\text{send}}(v) \text{ 返回} \iff R_{\text{recv}} \text{ 已就绪}

1. 发送动作:发送方执行 ch <- v,把消息 v 递向通道。

Ssend(v)S_{\text{send}}(v)

2. 返回条件:当且仅当接收方已准备好,发送操作才真正返回。

  ⟺  Rrecv 已就绪\iff R_{\text{recv}} \text{ 已就绪}

3. 同步语义:消息不是先放进某个地方,而是双方同时握手交付。

S∥R⇒交接完成S \parallel R \Rightarrow \text{交接完成}

正因为这种面交特性,无缓冲 channel 常被当作信号灯:主 goroutine 想等某个 worker 干完活,就创建一个无缓冲 channel,worker 干完时往里发一条空消息,主 goroutine 收到消息就知道活干完了——就像你收到快递员递来的包裹,就知道他已经到了。

有缓冲异步:格口的异步缓冲

给 make 加上容量参数 make(chan int, 3),就得到带 3 个格口的有缓冲 channel。这模仿了小区里的快递柜:快递员把包裹塞进空格口,拍照回执转身就走,完全不必等收件人出现。收件人也是,有空了才来取,晚来几个小时也没关系。包裹暂时待在格口里,双方的时间不必对齐。

有缓冲 channel 的规则只有两条:格口满了,发送方必须等有人取走才能继续放;格口空了,接收方必须等有人放入才能继续取。换句话说,发送阻塞发生在 len 等于 cap 时,接收阻塞发生在 len 等于 0 时。在中间的任何时刻,双方都能按自己的节奏工作,这就是异步解耦:生产者不必等消费者,消费者也不必等生产者。

发送阻塞  ⟺  len=C,接收阻塞  ⟺  len=0\text{发送阻塞} \iff \text{len} = C, \qquad \text{接收阻塞} \iff \text{len} = 0

1. len 含义:len(ch) 表示当前格口中已存放的消息条数。

len=#{已存入消息}\text{len} = \#\{\text{已存入消息}\}

2. cap 含义:cap(ch) 表示格口总数,是 make 时定死的容量。

C=cap(ch)C = \text{cap}(ch)

3. 满则阻塞:当 len 等于 C,发送方投递新消息时必须等待空格口。

len=C⇒发送等待\text{len} = C \Rightarrow \text{发送等待}

4. 空则阻塞:当 len 等于 0,接收方想取消息时必须等待新消息入库。

len=0⇒接收等待\text{len} = 0 \Rightarrow \text{接收等待}

快递柜的比喻在这里完全成立:柜子的状态决定了你是“放得下”还是“要排队”。有缓冲 channel 的价值,就在于让生产和消费的节奏不必分秒对齐。

缓冲大小:格口数怎么定

快递柜到底要装多少个格口?装太少,快递员高峰总在排队;装太多,空着的柜子既占地方又增加成本。有缓冲 channel 的容量设计也是同样的权衡。

Go 提供了两个函数配合观察:len(ch) 返回当前已存消息数,cap(ch) 返回总容量。负载率 = len/cap 接近 1 时,说明生产速度逼近消费速度,随时可能触发阻塞;接近 0 时说明消费能力富余。监控系统定时读取 len,就能画出队列水位线,指导是否扩容消费者或调整缓冲大小。经验上,工作池的任务队列常用“固定任务数”或“CPU 核数”,日志管道可以适当给大一些做削峰,但不宜过大——那会拖长延迟并浪费内存。

ρ=len(ch)C×100%,空闲=C−len(ch)\rho = \frac{\text{len}(ch)}{C} \times 100\%, \qquad \text{空闲} = C - \text{len}(ch)

1. 当前存量:len(ch) 反映此刻柜子里已经塞了多少条消息。

len(ch)\text{len}(ch)

2. 总容量:C 是 make 时设定的格口总数。

C=cap(ch)C = \text{cap}(ch)

3. 负载率:已用除以总量得到负载率 ρ,衡量柜子紧张程度。

ρ=len(ch)C\rho = \frac{\text{len}(ch)}{C}

4. 空闲格口:总量减已用,得到还能再扛多少条消息。

空闲=C−len(ch)\text{空闲} = C - \text{len}(ch)

挑选缓冲大小没有银弹,奥妙在于用 len/cap 持续观察柜子的“水位”。水位长期偏高就加格口或加消费者,长期偏低就缩格口省内存。有了监控,格口数量就能持续调优。

死锁排查:谁在等谁

假设两个快递员约好面交:小张拿着包裹在城南等小刘,小刘拿着另一个包裹在城北等小张。两人都以为对方会过来,结果谁都不会动,这场交接永远无法开始。Go 里的 channel 死锁就是这种“互相等待且无人推进”的状态——程序里所有 goroutine 都阻塞在收发操作上,系统发现再也无人能运行,立即报错 all goroutines are asleep - deadlock。

最常见的三种死锁场景。一是只发不收:一个 goroutine 往无缓冲 channel 发消息,但永远没有接收方,它自己就挂住了。二是自己等自己:在同一个 goroutine 里先向无缓冲 channel 发送、又等待从它接收,等于左手递右手,没有第三个人在场。三是循环等待:A 等 B 的 channel、B 等 C 的 channel、C 又等 A 的 channel,形成一个等待环。工具方面,go vet 能静态发现部分问题,运行时 panic 会打印所有 goroutine 的栈信息,pprof 的 goroutine profile 则能帮你画出等待关系。

死锁  ⟺  (∀g∈G:blocked(g))∧(∄ 可匹配的收发对)\text{死锁} \iff (\forall g \in G: \text{blocked}(g)) \land (\nexists\,\text{可匹配的收发对})

1. 全部阻塞:集合 G 中的每个 goroutine 都卡在 channel 操作上。

∀g∈G:blocked(g)\forall g \in G: \text{blocked}(g)

2. 无可推进:不存在一对已经就绪、可以完成交接的收发双方。

∄ 可匹配的收发对\nexists\,\text{可匹配的收发对}

3. 死锁判定:两个条件同时成立,系统进入永远等不到的状态。

死锁  ⟺  全部阻塞∧无可推进\text{死锁} \iff \text{全部阻塞} \land \text{无可推进}

排查死锁,本质上是画一张“谁在等谁”的图:每个阻塞点问一句“它等的是什么,那件事什么时候会发生”。只要找到打破等待环的那一步——补一个接收方、换有缓冲 channel、或者调整等待顺序——程序就活过来了。

安全关闭:正确贴封条

快递柜完成使命要停用时,物业会在柜门上贴封条,同时贴出“本柜停用”的公告。Go 里这个动作叫 close(ch),它告诉所有接收方:不会再有新包裹了。封条贴得好不好,直接影响后续取件的人怎么判断。

关闭 channel 有几条铁律。第一,只能在发送方关闭,不能在接收方关闭——接收方不知道是否还有其他发送方,贸然贴封条会让别的快递员往停用柜里塞包裹,触发 panic。第二,向已关闭的 channel 再发送会 panic,所以关闭前要确保没有代码再执行发送。第三,接收方用 v, ok := <-ch 判断:ok 为 false 表示通道已关闭且格口取空,此时 v 是零值,就像取到一个空包裹。用 for range 遍历 channel,也会在关闭且取空后自动退出,省去手动判断。

ok={true,ch 未关闭false,ch 已关闭且取空ok = \begin{cases} \text{true}, & ch \text{ 未关闭} \\ \text{false}, & ch \text{ 已关闭且取空} \end{cases}

1. 接收取值:无论通道是否关闭,<-ch 总能取出一个值。

v,ok←chv, ok \leftarrow ch

2. ok 标记:ok 是取值时附带的状态位,表示这次取值是否有效。

ok=通道状态标记ok = \text{通道状态标记}

3. 判断语义:通道正常时 ok 为 true;已关闭且取空时 ok 为 false,v 自动落到零值。

ok=false⇒通道已停用\text{ok=false} \Rightarrow \text{通道已停用}

关闭 channel 不是“销毁柜子”,而是向所有接收方广播“停止供货”的信号。配合 comma-ok 和 range,接收方就能优雅收工,这是并发程序退出阶段最常见的写法。

单向约束:专用投递口

小区快递柜通常有两个特殊口:投递口只进不出,取货口只出不进。这样设计是为了分工明确——快递员不该从取货口掏东西,收件人也不该往投递口塞包裹。Go 的单向 channel 把这种约束写进了类型系统。

声明方式:chan<- T 表示只写 channel,只能执行发送;<-chan T 表示只读 channel,只能执行接收。在函数参数上使用单向 channel,等于告诉调用方“我只会往这条管子里放数据”或“我只会从里面取数据”。一个双向 channel 在传给函数时能自动收窄成单向的,但反过来不行——单向不能还原成双向,从类型层面杜绝误操作。例如 func worker(jobs <-chan int, results chan<- int) 既让 worker 只能读任务,又只能写结果,连意外写反都在编译期报错。

chan ⁣<−T:只写,<−chan T:只读\mathrm{chan\!<- T}: \text{只写}, \qquad <-\mathrm{chan\,T}: \text{只读}

1. 只写方向:箭头指向 chan 右边,表示这个通道只能用于发送。

chan ⁣<−T:write-only\mathrm{chan\!<- T}: \text{write-only}

2. 只读方向:箭头在 chan 左边,表示这个通道只能用于接收。

<−chan T:read-only<-\mathrm{chan\,T}: \text{read-only}

3. 自动收窄:双向 channel 传参时自动变成单向,函数内外互不越权。

双向⇒单向\text{双向} \Rightarrow \text{单向}

4. 不可逆约束:单向 channel 无法变回双向,误用会在编译期报错。

单向⇏双向\text{单向} \not\Rightarrow \text{双向}

单向 channel 本质上是把“投递口”和“取货口”分开造,让每个 goroutine 手里只拿自己该拿的方向。设计良好的并发接口,往往一看到函数签名就能明白数据的流向。

AI 应用场景

AI推理服务并发结果汇总

阶段:模型推理与后处理

解决的问题:AI 推理服务常把大图切成多块并行推理,再把结果汇总。传统做法用共享切片加锁收集结果,代码容易出错,还要额外处理等待所有子任务完成的问题。无缓冲 channel 天然能做同步屏障:每个子任务完成后往 channel 发一条信号,主 goroutine 收齐 N 条信号再统一汇总,既不需要锁,也不会漏等。

具体案例:一个图像分割服务把 2048×2048 的图切成 4 块分别送 GPU 推理,4 个 worker 各自完成后向一个无缓冲 channel 发送完成信号,主 goroutine 连续收 4 次信号后开始拼接结果。任一 worker 没完成,主 goroutine 就安心等待,完美复用“面交”语义。

训练数据预处理流水线

阶段:数据加载与增强

解决的问题:训练样本的图片增强是 CPU 密集操作,串行处理 10 万张图要数小时。用有缓冲 channel 做任务队列,主线程只管把图片路径塞进 channel,多个 worker 并发从 channel 取任务处理,生产者和消费者速率天然解耦。

具体案例:一个 CV 训练管线设置 taskCh := make(chan string, 64),调度线程预先把 10 万条路径写入队列(队列满时自然等待),8 个 worker 并发消费。监控脚本每秒读一次 len(taskCh)/cap(taskCh),负载率超过 0.8 就自动扩容 2 个 worker,低于 0.2 则缩容,让预处理吞吐保持稳定。

LLM 网关日志削峰管道

阶段:服务观测与日志收集

解决的问题:高并发 LLM 网关每个请求都要记录 token 用量和延迟,如果同步写日志文件,IO 慢会拖垮响应。用有缓冲 channel 做日志管道,请求线程只把日志丢进 channel 就继续干活,后台一个 writer goroutine 批量落盘。

具体案例:一个 OpenAI 兼容代理用 logCh := make(chan LogEntry, 256) 缓冲日志,请求线程 logCh <- entry 无阻塞;writer 每次取 32 条合并写入文件,大幅减少磁盘 IO。服务关闭时先 close(logCh) 再让 writer 用 for range 消费完剩余日志,优雅 flush 后退出,一条日志都不丢。

Python 演示

python85 行
import queueimport threadingimport time  class UnbufferedChannel:    """无缓冲 channel:收发双方必须同时就绪(会合)"""     def __init__(self):        self._send_ready = threading.Event()        self._recv_ready = threading.Event()        self._value = None     def send(self, value):        self._value = value        self._send_ready.set()       # 告诉接收者:我已就绪        self._recv_ready.wait()      # 等接收者取走再返回        self._recv_ready.clear()     def recv(self):        self._send_ready.wait()      # 等发送者就绪        v = self._value        self._recv_ready.set()       # 告诉发送者:已取走        self._send_ready.clear()        return v  def demo_unbuffered():    ch = UnbufferedChannel()    start = time.time()     def log(msg):        print(f"[{time.time() - start:6.2f}s] {msg}")     def sender():        log("发送者:携带消息出发")        ch.send("hello")             # 接收者没到场就阻塞        log("发送者:交接完成")     def receiver():        time.sleep(0.8)              # 模拟接收者晚到        log("接收者:到场开始接收")        data = ch.recv()        log(f"接收者:收到 {data!r}")     t1 = threading.Thread(target=sender)    t2 = threading.Thread(target=receiver)    t1.start()    t2.start()    t1.join()    t2.join()  def demo_buffered():    ch = queue.Queue(maxsize=2)      # 有缓冲 channel,容量 2    start = time.time()     def log(msg):        print(f"[{time.time() - start:6.2f}s] {msg}")     def sender():        for i in range(3):            ch.put(f"msg-{i}")       # 前两条立即放入,第三条阻塞            log(f"发送者:放入 msg-{i},len={ch.qsize()}/2")     def receiver():        time.sleep(1.2)              # 模拟收件人晚来        for _ in range(3):            data = ch.get()            log(f"接收者:取出 {data!r}")     t1 = threading.Thread(target=sender)    t2 = threading.Thread(target=receiver)    t1.start()    t2.start()    t1.join()    t2.join()  if __name__ == "__main__":    print("==== 无缓冲:面交模式 ====")    demo_unbuffered()    print()    print("==== 有缓冲:快递柜模式 ====")    demo_buffered()
目的:用 Python 原语复刻 Go channel 的两种核心语义,帮助你直观感受“面交”与“快递柜”的时序差异。
  1. ch = UnbufferedChannel()创建无缓冲通道,内部用两个 Event 实现会合点。
  2. ch.send("hello")发送方发出消息,在接收方未就绪时会被阻塞住。
  3. self._recv_ready.wait()发送方等待“接收方已取走”的信号,保证取走才算完成。
  4. data = ch.recv()接收方开始接收,若发送方未就绪则阻塞等待。
  5. self._send_ready.wait()接收方等待“发送方已就绪”的信号,两者对齐才交接。
  6. ch = queue.Queue(maxsize=2)有缓冲通道,等价于 make(chan string, 2)。
  7. ch.put(f"msg-{i}")放入格口;当 2 个格口全满时,第三次放入会阻塞。
  8. ch.qsize()查看当前已存消息数,对应 Go 里的 len(ch)。
  9. ch.get()从格口取走消息;格口全空时取操作会阻塞。
  10. time.sleep(1.2)模拟收件人晚到,让有缓冲模式的“放完就走”充分显现。

常见误区

误区无缓冲 channel 是不是就像一个容量为 0 的队列,什么也存不下,所以只能看不能用?

正解无缓冲 channel 不是“容量为 0 的队列”,而是“根本没有队列”。它不需要存货——数据直接从发送方手里递到接收方手里,就像面交时快递员把包裹直接交给你,中间没有柜子。只要双方同时就绪,数据就能正常传递;它不仅能传数据,还常用于 goroutine 之间的同步信号。所谓“无缓冲”指的是没有中转格口,而不是不能通信。

为何常见:初学者看到 make(chan int) 没有容量参数,容易类比成“数组长度为 0”,忽略了 channel 的管道本质,把它当成一种存储结构。

误区既然容量是 10,前 10 次发送都不需要等接收方,那只要容量设得够大,发送不就不会卡住了吗?

正解有缓冲只是把阻塞点向后推。当 len 等于 cap 时,下一次发送照样阻塞,直到接收方取走一个格口腾出空间。缓冲解决了“每一笔都要等对方”的同步等待,但无法消除“总体产能不匹配”时的排队等待。如果生产者持续比消费者快,无论缓冲多大,最终都会被填满然后开始阻塞。

为何常见:把“缓冲”误解成“无限缓存”,没意识到容量边界正是缓冲的本质属性,有边界就有满的时候。

误区接收方用 ok 能感知关闭,那关闭后继续往里发数据,接收方顶多收到 ok=false 吧,应该不会出大问题?

正解向已关闭的 channel 发送数据会直接触发 panic: send on closed channel。channel 关闭的正确语义是“我不再供货了”,而不是“我拒收新货但你可以继续塞”。发送方要自己保证关闭后不再发送,通常由唯一负责关闭的发送方在最后执行 close,其他发送方配合同步机制(如 WaitGroup)确认全部结束后再关。

为何常见:把 close 类比成“结束标记”,觉得只是个标志位;没意识到它对发送方是一条硬性禁令,违反就 panic。

误区channel 关闭后没人再往里发消息,那 len 会慢慢变成 0,等 len 为 0 时不就说明通道关闭了吗?

正解len 只表示当前格口中的积压消息数,与关闭状态毫无关系。关闭后如果格口里还有存货,len 仍然大于 0;反过来,发送方刚把消息都取走时 len 为 0,但 channel 还开着。要判断是否关闭,唯一可靠的方式是 v, ok := <-ch 中 ok 的值,或者用 for range 循环自然退出。

为何常见:试图用一个“快照指标”推断一个“状态”,而 len 本身就不是为判断关闭设计的,它只回答“现在积压了多少”。

误区调大 `make(chan int, N)` 的 N 能明显减少阻塞,那 N 越大并发越快,直接设一个很大的数不就行了?

正解缓冲过大有两个隐患。一是内存被长期占据:消息在格口里待得久,积压数据占着内存不清空。二是延迟变高:生产者放完消息,消息不会立刻到消费者手里,而是在柜子里等,端到端延迟被拉长。更隐蔽的是,过大的缓冲会掩盖生产者和消费者的速率失衡,让问题迟迟不暴露。正确做法是用 len/cap 监控水位,让负载率维持在一个健康区间。

为何常见:只看到阻塞减少的收益,忽略了缓冲的本质是“用空间换时间”,而空间和时间都不是免费的。

面试题

Q1. 无缓冲 channel 的发送操作在什么情况下会被阻塞?

单选题 · easy

  • A.缓冲区已经全部填满
  • B.没有接收者同时就绪← 正确答案
  • C.接收者正在执行任务
  • D.缓冲区完全为空时

答案:B。无缓冲 channel 没有缓冲区,发送完成的唯一条件是接收方就绪,所以选 B。A 和 D 都是针对有缓冲 channel 的说法,本题是无缓冲场景;C 是干扰项,接收者“正在执行任务”不代表它没在 <-ch 上等待。

Q2. make(chan int, 3) 创建的是什么?

单选题 · easy

  • A.只能收发三次的通道
  • B.容量为 3 的缓冲通道← 正确答案
  • C.最多三个协程通信
  • D.无缓冲的同步通道

答案:B。第二个参数是缓冲区容量,cap(ch) 为 3,所以选 B。它不限收发次数,也不限协程数量,因此 A、C 错误;无缓冲 channel 的写法是不带容量参数,D 错误。

Q3. 有缓冲 channel 的 len(ch) 函数返回什么?

单选题 · easy

  • A.缓冲区容量的大小
  • B.当前已存入的元素数← 正确答案
  • C.等待发送的协程数量
  • D.通道底层数组长度

答案:B。len(ch) 返回当前格口中积压的消息条数,对应快递柜里已用的格口数,选 B。容量大小由 cap(ch) 返回;等待发送的协程数量既不是 len 也不是 cap;channel 底层结构对外不可见。

Q4. 有缓冲 channel 的 cap=5、len=5 时,再执行发送会怎样?

单选题 · mid

  • A.立即成功不被阻塞
  • B.阻塞直到腾出空间← 正确答案
  • C.抛出 panic 异常
  • D.自动覆盖最早消息

答案:B。len 等于 cap 表示柜子已满,发送方必须等接收方取走一条消息腾出格口才能继续,选 B。向已关闭 channel 发送才会 panic,C 错误;channel 没有覆盖旧数据的语义,D 是别的队列结构的行为。

Q5. 无缓冲 channel 最典型的应用场景是什么?

单选题 · mid

  • A.异步任务解耦与缓存
  • B.协程之间的同步屏障← 正确答案
  • C.消息队列持久化存储
  • D.替代全局变量来共享

答案:B。无缓冲 channel 要求双方同时就绪,天然适合做同步屏障,让主 goroutine 等待 worker 完成信号,选 B。A、C 是有缓冲 channel 的典型用途;D 表述错误,channel 的哲学是“通过通信共享内存”,而不是“用通道替代变量共享”。

Q6. 向已关闭的 channel 发送数据会发生什么?

单选题 · mid

  • A.数据被静默丢弃掉
  • B.触发 panic 异常← 正确答案
  • C.发送方阻塞等待中
  • D.自动重试直到成功

答案:B。已关闭的 channel 相当于贴了封条的柜子,再往里塞包裹会直接 panic: send on closed channel,选 B。缓冲满导致的才是阻塞,C 错误;channel 没有静默丢弃和自动重试机制,A、D 都不存在。

Q7. 如何优雅判断一个 channel 是否已关闭?

单选题 · mid

  • A.检查 len 是否为零
  • B.用逗号 ok 接收判断← 正确答案
  • C.读取 cap 容量大小
  • D.向通道发送探测数据

答案:B。v, ok := <-ch 中 ok 为 false 表示通道已关闭且数据取空,这是唯一可靠的判断方式,选 B。len 只反映积压量,cap 是固定常量,两者都与关闭状态无关;向通道发探测数据会 panic,D 非常危险。

Q8. 以下哪些做法能有效避免 channel 死锁?

多选题 · mid

  • A.只在发送方关闭通道← 正确答案
  • B.保证接收方最终就绪← 正确答案
  • C.合理设置缓冲的大小← 正确答案
  • D.发送后立即关闭通道

答案:ABC。A 避免“关闭后继续发送”的 panic,B 打破“发送等接收、接收等发送”的等待环,C 降低满格口阻塞的概率,三者都能预防死锁。D 是坏习惯:发送后立刻关闭,其他发送方再发就会 panic,应等所有发送方结束后再统一关闭。

Q9. 关于单向 channel,下列说法正确的有哪些?

多选题 · hard

  • A.chan<- 声明为只写通道← 正确答案
  • B.<-chan 声明为只读通道← 正确答案
  • C.单向可随意转成双向
  • D.常作函数参数来约束← 正确答案

答案:ABD。chan<- T 只能发送,<-chan T 只能接收,A、B 正确。单向 channel 常用于函数参数,把数据流向写进类型,D 正确。C 错误:Go 只允许双向转单向,单向不能还原成双向,这正是为了编译期防错。

Q10. 以下哪些场景适合选用有缓冲 channel?

多选题 · extreme

  • A.并发工作池的任务队列← 正确答案
  • B.需要精确同步的信号
  • C.日志流水线削峰填谷← 正确答案
  • D.生产者消费者解耦场景← 正确答案

答案:ACD。工作池、日志削峰、生产者消费者解耦都需要吸收双方速率差,有缓冲 channel 的格口正好派上用场,A、C、D 正确。B“精确同步”要求双方同时在场,应使用无缓冲 channel 做同步屏障;有缓冲会让信号时间点模糊,无法精确对齐。