Go 基础体系 · 第 18/113 篇。示例统一基于 Go 1.26.4;核心片段可能省略 package 与 import,完整程序可直接按文中结构运行。
Go channel 完整基础:发送、接收、缓冲、关闭与所有权
本文所有语义与示例均以 Go 1.26.4 为基准。channel 是 Go 用来在 goroutine 之间传递值并建立同步关系的类型化通信原语。它可以承载任务、结果、事件和生命周期信号,但不是“线程安全队列”的简单别名,也不能自动消除经由指针、slice 或 map 产生的共享内存竞争。
真正完整的 channel 设计必须回答六个问题:谁创建、谁发送、谁接收、谁关闭、阻塞怎样被取消、错误怎样返回。只会写 ch <- value 和 <-ch,还不足以保证并发程序能够结束、不会泄漏并且在过载时行为可控。
1. Channel 类型、方向、创建与零值
chan T 表示可发送也可接收的 channel,chan<- T 只能发送,<-chan T 只能接收。方向是编译期能力约束:双向 channel 可以隐式转换为单向 channel,单向 channel 不能反向恢复成双向类型。
func produce(out chan<- int) {
out <- 42
}
func consume(in <-chan int) int {
return <-in
}
func main() {
values := make(chan int)
go produce(values)
fmt.Println(consume(values))
}
make(chan T) 创建无缓冲 channel;make(chan T, n) 创建容量为 n 的缓冲 channel。容量不能为负,运行时会为 channel 本身以及可能存在的缓冲区分配空间。
channel 变量保存的是对运行时 channel 对象的引用。把 channel 赋值给另一个变量或作为参数传递,只复制引用,不会复制缓冲区。channel 可与 nil 以及相同元素类型的 channel 比较;只有引用同一个运行时对象时才相等。
channel 的零值是 nil。对 nil channel 发送或接收会永久阻塞,关闭 nil channel 会 panic。它与“已创建但没有数据”的空 channel 不是同一状态。
var missing chan int
fmt.Println(missing == nil) // true
ready := make(chan int)
fmt.Println(ready == nil) // false
len(ch) 返回采样瞬间缓冲区中的元素数,cap(ch) 返回固定容量。另一个 goroutine 可以在下一纳秒改变长度,因此不能写成“先判断 len,再决定是否发送或接收”的正确性协议。len 适合观测,不适合加锁式判断。
2. 运行时模型与数据流:缓冲区、发送等待队列和接收等待队列
从语义上理解 channel 时,可以把一个非 nil channel 看成四部分:元素类型信息、可选的环形缓冲区、等待发送的 goroutine 队列、等待接收的 goroutine 队列。运行时实现通常用 hchan 一类内部结构维护这些状态,并用锁保护结构本身;这些实现细节不是公开 API,业务代码不能依赖字段布局或等待队列的具体策略。
发送发生时,运行时大致按以下顺序寻找完成条件:
- 如果已有接收者等待,值直接交给该接收者;
- 否则如果缓冲区还有空位,把值复制进缓冲区;
- 否则把当前 goroutine 挂入发送等待队列并让出执行权。
接收则是对称流程:
- 如果已有发送者等待,完成一次交接;
- 否则如果缓冲区非空,取出队头元素;
- 否则在 channel 尚未关闭时挂入接收等待队列;
- 若 channel 已关闭且缓冲耗尽,立即返回零值与
ok=false。
“阻塞”不等于占住操作系统线程空转。goroutine 会进入等待态,调度器可让其他 goroutine 继续运行。不过等待中的 goroutine 仍保留栈、引用和调度元数据;如果协议永远不再满足,它就是泄漏。
3. 发送语义:先求值,再等待通信完成
发送语句 ch <- expression 会先求值 channel 操作数和右侧表达式,然后尝试通信。如果通信暂时不能完成,已经求出的值会随等待中的发送操作保留。
func build() int {
fmt.Println("build once")
return 7
}
values := make(chan int)
go func() {
time.Sleep(10 * time.Millisecond)
fmt.Println(<-values)
}()
values <- build()
一次发送会复制 channel 元素值。发送整数、结构体时复制完整值;发送指针时复制地址;发送 slice、map、函数或接口时复制描述符。复制描述符不等于复制底层数据,所以“通过 channel 发送过”不能证明底层对象可被两端并发修改。
发送到已关闭 channel 会 panic,关闭状态无法通过一个无竞态的 isClosed 检查提前规避。即使某个检查函数在此刻返回未关闭,另一个 goroutine 也可能紧接着关闭。因此正确方案是设计唯一且可证明的关闭所有者,而不是检查后发送或用 recover 掩盖协议错误。
4. 接收语义:三种写法和 comma-ok
接收表达式可以丢弃结果、接收一个值,或同时接收值和状态:
<-events
value := <-values
value, ok := <-values
ok=true 表示收到的是某次发送产生的值;ok=false 表示 channel 已关闭且缓冲数据已经取完。元素零值可以是合法数据,所以不能只看 value == 0、value == "" 或 value == nil 判断关闭。
values := make(chan int, 2)
values <- 0
values <- 8
close(values)
for {
value, ok := <-values
if !ok {
break
}
fmt.Println(value) // 合法数据 0 和 8
}
for value := range values 会持续接收,直到 channel 关闭并被排空。它不会因为 channel 暂时为空而结束。反过来,如果生产者永远不关闭,而消费者又依赖 range 结束,消费者就会永久等待。
for value := range values {
fmt.Println(value)
}
是否使用 range 取决于协议:当“关闭输出”明确表示数据流结束时,它很合适;当消费者知道精确数量,或生命周期完全由 context 控制时,不一定需要关闭后再 range。
5. 无缓冲 channel:发送与接收的一次会合
无缓冲 channel 没有存放元素的槽位。发送必须等某个接收操作准备完成,接收也必须等发送者出现。它建立的是一次 rendezvous,也就是发送方和接收方在某个通信点会合,而不是把值先放进普通队列。
ready := make(chan struct{})
go func() {
prepareResource()
close(ready)
}()
<-ready
useResource()
无缓冲并不意味着两个 goroutine 会同时继续。通信完成后,哪一个先获得 CPU 仍由调度器决定。业务只能依赖通信建立的先后关系,不能依赖日志看起来总是谁先打印。
同一个 goroutine 向无缓冲 channel 发送,然后在后续语句接收,会在发送处阻塞,后面的接收永远无法执行:
func deadlock() {
ch := make(chan int)
ch <- 1
fmt.Println(<-ch) // 永远到不了
}
修复方式不是随意加一个大缓冲,而是明确另一个并发参与者,或者承认这里根本不需要 channel。
6. Channel 与 Go 内存模型的 happens-before
channel 不只是传值,还建立内存可见性关系。Go 内存模型规定:
- 某次发送完成,发生在对应接收完成之前;
- 关闭 channel,发生在因该关闭而返回零值的接收完成之前;
- 对容量为
C的 channel,第k次接收发生在第k+C次发送完成之前。
因此,可以在发送或关闭之前写入普通变量,再在完成对应接收后读取:
done := make(chan struct{})
var message string
go func() {
message = "ready"
close(done)
}()
<-done
fmt.Println(message)
这里的可见性来自 close(done) 与 <-done 的同步关系。若读取发生在接收之前,或另一个 goroutine 在同步完成后继续写 message,仍可能出现数据竞争。
channel 传递指针时尤其容易误判:发送之前完成的对象写入能被接收方看到,但如果发送方在发送之后继续修改同一对象,而接收方也在访问,就需要额外同步。常见安全约定是“发送即转移所有权”,发送者之后不再访问可变对象。
7. 缓冲 channel:有限解耦,不是无限队列
缓冲 channel 在未满时允许发送立即完成,在非空时允许接收立即完成。容量是“允许多少项已接纳但尚未消费的工作”,不是 worker 数量,也不是吞吐量。
queue := make(chan string, 2)
queue <- "A"
queue <- "B"
fmt.Println(len(queue), cap(queue)) // 2 2
fmt.Println(<-queue)
fmt.Println(<-queue)
容量为 1 常用于只返回一次结果的异步操作:即使调用方因超时不再接收,worker 仍能完成一次发送并退出。它不能取消外部 I/O,真正的网络或数据库调用仍要接收 context。
容量大于 1 可以吸收短时调度抖动或上下游速率波动,但消费者永久退出后,任何有限缓冲最终都会写满。单纯扩大容量通常只是把“立即阻塞”变成“更晚阻塞”,同时增加内存与排队延迟。
8. 容量设计与背压:从预算反推,不靠猜
确定 channel 容量时,应从业务预算反推:允许多少任务等待、每项占多少内存、可接受的最大排队时间、峰值输入速率、稳定处理速率,以及队列满时采取什么策略。
假设峰值每秒进入 1,000 项,稳定消费每秒 800 项,峰值持续 5 秒,期间会积压约 1,000 项。若每项连同引用对象占 20 KiB,仅队列引用的存活集合就可能接近 20 MiB;如果每项还持有请求体或图片,成本会更高。容量选择必须与延迟和内存预算一起评审。
常见过载策略有三类:
- 阻塞生产者,把压力沿调用链向上游传播;
- 非阻塞拒绝,立即返回“系统繁忙”并计数;
- 有业务依据地丢弃旧值或低优先级值。
select {
case queue <- job:
return nil
default:
return ErrQueueFull
}
default 会把发送变成非阻塞尝试,但它也可能在极短暂拥塞时拒绝任务。是否采用必须由产品语义决定,不能为了“防阻塞”随处添加。
9. 关闭语义:关闭的是发送阶段,不是对象
close(ch) 表示“以后不会再有新的发送”。它不是释放内存、清空缓冲或强制取消发送者。channel 对象会像其他可达对象一样由垃圾回收器管理,不需要为了 GC 而关闭。
关闭后的行为可以归纳为下表:
| 操作 | nil channel | 开放且可立即通信 | 开放但暂不可通信 | 已关闭 |
|---|---|---|---|---|
| 发送 | 永久阻塞 | 成功 | 阻塞 | panic |
| 接收 | 永久阻塞 | 得到发送值 | 阻塞 | 先排空缓冲,再返回零值、ok=false |
| 关闭 | panic | 成功 | 成功并唤醒接收者 | panic |
关闭时,缓冲区中的值不会丢失;阻塞接收者会被唤醒;阻塞发送者不会被当作成功发送,而会因向已关闭 channel 发送而 panic。因此绝不能把“接收方关闭输入”当作粗暴取消手段。
values := make(chan int, 2)
values <- 7
values <- 9
close(values)
fmt.Println(<-values) // 7
fmt.Println(<-values) // 9
value, ok := <-values
fmt.Println(value, ok) // 0 false
10. 所有权原则:由能证明“不再发送”的一方关闭
关闭权不简单等于“谁创建谁关闭”,而属于能证明所有发送已经结束的一方。单生产者最直接:生产 goroutine 在退出时关闭自己的输出。
func generate(ctx context.Context, values []int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, value := range values {
select {
case out <- value:
case <-ctx.Done():
return
}
}
}()
return out
}
多生产者共享输出时,任何一个生产者都无法单独证明其他人已经停止,因此生产者本身不应各自关闭。由协调者等待全部生产者退出,再执行一次关闭:
func mergeWorkers(ctx context.Context, workers int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for id := range workers {
wg.Go(func() {
for n := range 3 {
select {
case out <- id*10 + n:
case <-ctx.Done():
return
}
}
})
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Go 1.26.4 的 WaitGroup.Go 会把任务加入组并在函数返回时完成计数,传入函数不应 panic。使用传统 Add/Done 时,必须在启动 goroutine 前完成 Add,避免 Wait 与新增任务发生错误交错。
11. 取消与关闭必须分工
数据 channel 的关闭表示数据流结束;context.Context 表示调用链取消或 deadline;error 表示失败原因。三者职责不同。
下游不再消费时,不能关闭上游正在发送的 channel。它应该取消 context,让发送者在阻塞发送时也能退出:
select {
case results <- result:
return nil
case <-ctx.Done():
return ctx.Err()
}
只在循环入口调用一次 ctx.Err() 不够,因为 goroutine 可能随后永久阻塞在 channel 操作。任何可能持续等待的发送、接收或外部 I/O,都应有可达的取消分支。
广播“一次性完成”可关闭 chan struct{},因为所有等待接收的 goroutine 都会被唤醒。但现代请求链路通常应优先使用 context,它能携带 deadline 和取消原因,并可沿调用树传播。
12. nil channel 在 select 中用于动态禁用分支
普通代码中遇到 nil channel 往往是初始化错误;在 select 中,nil channel 对应 case 永远不会就绪,因此可以通过把局部 channel 变量设为 nil 动态启用或禁用分支。
func relay(input <-chan int, output chan<- int) {
var pending []int
for input != nil || len(pending) > 0 {
var send chan<- int
var next int
if len(pending) > 0 {
send = output
next = pending[0]
}
select {
case value, ok := <-input:
if !ok {
input = nil
continue
}
pending = append(pending, value)
case send <- next:
pending = pending[1:]
}
}
}
如果已关闭输入仍保留在 select 中,它会持续立即返回零值,形成高 CPU 循环并压制其他 case。处理关闭后应将该局部 channel 设为 nil 或直接退出。若所有 case 都因 nil 被禁用且没有 default,当前 goroutine会永久阻塞。
13. 方向化 API 让通信能力可见
函数参数和返回值使用单向 channel,可以把“此函数只发送”或“调用方只能接收”变成编译器可检查的约束。
func sendAll(ctx context.Context, out chan<- string, values []string) error {
for _, value := range values {
select {
case out <- value:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func first(in <-chan string) (string, bool) {
value, ok := <-in
return value, ok
}
方向并不表达唯一发送者,也不会阻止另一个仍持有双向引用的代码关闭。API 文档仍需说明:谁关闭、调用方提前停止消费时如何通知生产者、错误从哪里返回、值的所有权是否转移。
返回只读 channel 的函数通常已经启动 goroutine。调用方必须知道如何终止它,否则一个看似简单的返回值会隐藏生命周期成本。若操作本质上是同步一次调用,直接返回 (T, error) 往往更清晰。
14. 多发送者、多接收者与顺序边界
多个 goroutine 可以并发向同一 channel 发送或从中接收,运行时会保护 channel 内部状态。这个“操作安全”不等于业务顺序确定。
单个发送者按其发送操作实际完成的顺序提交值;多个发送者之间的全局顺序由调度、阻塞和 select 就绪情况决定。多个接收者中,某个值只交给一个接收操作,channel 不会自动广播同一个值。
运行时不会承诺可供业务依赖的严格公平调度。即使等待队列实现通常倾向按排队顺序唤醒,也可能受 select、抢占和重新调度影响。需要按用户、分区或序列号有序处理时,应显式分片到独立 channel,或由单一所有者排序,而不是依赖“测试时看起来很公平”。
15. 值所有权:channel 安全不等于载荷安全
以下代码对 channel 的操作本身安全,但对 map 的并发访问不安全:
updates := make(chan map[string]int, 1)
state := map[string]int{"count": 1}
updates <- state
go func() {
received := <-updates
received["count"]++
}()
state["other"] = 2 // 与上面的 map 写入可能竞争
常用载荷规则有三种:
- 发送后转移所有权,发送方不再读写;
- 发送不可变快照,例如复制 map 或 slice 的底层数据;
- 两端共享,但对象内部另有 Mutex、atomic 或线程安全抽象。
传大结构体可避免共享,却可能增加复制成本;传指针减少复制,却可能增加逃逸、GC 扫描和所有权复杂度。应先选正确语义,再用 benchmark 与 profile 决定是否优化。
16. Channel、Mutex、Cond 与 atomic 的选型边界
channel 适合传递任务、结果、事件和对象所有权,或让单个 goroutine 串行拥有复杂状态。Mutex 适合多个 goroutine 对内存中的短临界区做同步访问。atomic 适合少量、语义明确的独立状态;Cond 适合在持锁条件变化时唤醒等待者。
如果缓存核心 API 是同步 Get/Set,一把 RWMutex 通常比“启动常驻 goroutine,再为每次读取构造请求和响应 channel”更直接。如果状态变化天然是一串命令,并且串行顺序本身就是领域约束,单一拥有者加命令 channel 可能更清晰。
不要用“一切都通过 channel”或“一切都加锁”的口号代替建模。判断标准是:数据由谁拥有、调用是同步还是异步、是否需要背压、取消如何传播、临界区是否足够短。
17. Pipeline、fan-out 与 fan-in 的闭合协议
流水线中,每个 stage 通常读取输入、生成输出,并只关闭自己创建的输出。fan-out 让多个 worker 消费同一个输入;fan-in 把多个输出汇合为一个 channel。
func square(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for value := range in {
select {
case out <- value * value:
case <-ctx.Done():
return
}
}
}()
return out
}
func merge(ctx context.Context, inputs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, input := range inputs {
wg.Go(func() {
for value := range input {
select {
case out <- value:
case <-ctx.Done():
return
}
}
})
}
go func() {
wg.Wait()
close(out)
}()
return out
}
关键不是函数名,而是闭合关系:每个输出只有一个关闭者;下游提前退出时 context 能到达所有上游;协调者等待全部转发 goroutine 后才关闭汇总输出。缺少任意一项,都可能在异常路径泄漏。
18. 常见 panic、死锁与泄漏陷阱
channel 故障通常来自协议不闭合,而非运行时随机失效:
- 同一 goroutine 先向无缓冲 channel 发送、后接收,发送处自锁;
- 消费者
range等待关闭,生产者所有返回路径却没有关闭; - 下游只读取第一个结果就退出,上游仍无取消地发送;
- 多个发送者竞争关闭,出现
close of closed channel或send on closed channel; - 接收方关闭输入来“通知停止”,并发发送者随即 panic;
- 用
len(ch) < cap(ch)判断可发送,判断后状态变化,仍然阻塞; - 锁内执行可能阻塞的 channel 操作,而另一端需要同一把锁;
- select 持续读取已关闭 channel,产生零值忙循环;
- goroutine 捕获大对象后阻塞在 channel,使对象长期不能回收。
运行时只会在所有 goroutine 都无法继续且没有网络轮询等可唤醒事件时报告 fatal error: all goroutines are asleep - deadlock!。服务器中即使已经泄漏成千上万个 goroutine,只要仍有其他事件,进程通常不会自动报错。
19. 故障诊断:从现象还原通信协议
请求不返回、goroutine 数持续增长、队列长期接近容量或内存不下降时,先保留证据:
curl -s http://127.0.0.1:6060/debug/pprof/goroutine?debug=2 > goroutines.txt
go tool pprof http://127.0.0.1:6060/debug/pprof/goroutine
go test -race ./...
goroutine dump 中的 [chan send]、[chan receive] 和 [select] 指出等待点,但等待点不一定是根因。沿调用栈向上检查创建者和所有者:另一端是否已经返回、关闭责任是否遗漏、context 是否真正传入、队列为何满、消费者是否被更早的错误终止。
对比多个时间点的 dump:如果同一调用栈数量持续增长,通常是稳定泄漏。block profile 可观察 channel 和锁的累计阻塞时间,但需要提前设置采样率;go tool trace 可看到 goroutine 状态转换和调度延迟;竞态检测器能发现共享载荷的竞争,却不能证明协议不会死锁。
20. 测试与验证:用事件推进,不用 Sleep 猜调度
并发测试应验证可观察协议:输出最终关闭、取消后 goroutine 退出、满队列执行预定策略、多生产者只关闭一次、合法零值不会被误判为结束。不要用 time.Sleep 猜“另一个 goroutine 应该已经运行了”。
func TestGenerateStopsAfterCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
out := generate(ctx, []int{1, 2, 3, 4})
if got := <-out; got != 1 {
t.Fatalf("first value = %d", got)
}
cancel()
deadline := time.After(time.Second)
for {
select {
case _, ok := <-out:
if !ok {
return
}
case <-deadline:
t.Fatal("producer did not close output after cancellation")
}
}
}
测试本身要有超时,使错误协议能够收敛。再执行 go test -race -count=100 ./... 扩大调度交错覆盖。高次数通过不能数学证明并发正确,但能捕获部分偶发错误。需要精确顺序时,使用测试专用 barrier channel 明确推进步骤。
还可以在测试前后记录 runtime.NumGoroutine 发现明显泄漏,但该值受测试框架和后台任务影响,只能作为辅助。更可靠的方法是让被测组件暴露 Wait/Close 生命周期,并断言它在 deadline 内完成。
21. 性能基准:比较正确的方案,不只测每次操作
channel 操作包含同步、可能的元素复制、队列维护和调度。无竞争时的纳秒数字不能代表真实系统;基准应覆盖无缓冲交接、不同容量、不同生产者/消费者数量、载荷大小和取消路径。
func BenchmarkBufferedChannel(b *testing.B) {
ch := make(chan int, 256)
done := make(chan struct{})
go func() {
defer close(done)
for range ch {
}
}()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ch <- i
}
close(ch)
<-done
}
批处理可以摊薄通信成本,但会增加单项等待时间,并扩大取消后可能废弃的工作。大结构体改为指针可能降低复制,却可能引入堆分配和共享竞争。优化前同时查看 benchstat、CPU profile、内存 profile、阻塞 profile 和端到端延迟分位数。
22. 生产部署、观测与运维
生产中的 channel 通常是进程内队列,进程退出后内容不会持久化,也无法天然跨实例协调。要求任务不丢、可重放或跨服务消费时,应使用具备持久化和确认语义的消息系统,而不是把 channel 包装成“内存 MQ”。
重要队列至少观测以下指标:
- 当前长度与容量,以及接近满载的持续时间;
- 入队等待、队列等待和实际处理耗时;
- 接纳、拒绝、丢弃、重试和失败数量;
- 活跃 worker、goroutine 总数和取消原因;
- 服务关闭时剩余任务与 drain 用时。
len(ch) 可作为近似 gauge,但采样值不是一致快照。指标标签不能携带任务 ID、用户 ID 等高基数数据。告警应关注持续拥塞、处理速率下降和 goroutine 趋势,而不是单次短峰值。
优雅关闭时通常先停止接纳新任务,再取消或等待生产者,随后由所有者关闭输入,worker 排空或按 deadline 退出,最后关闭结果并等待协调者。顺序必须与业务的“允许丢弃还是必须处理完”一致。
23. 可运行综合示例:有界、可取消、单一关闭者
下面程序把核心原则放在一起:输入容量有界;生产者关闭 jobs;worker 不关闭共享 results;协调者等待全部 worker 后关闭 results;所有可能阻塞的通信都观察 context。
package main
import (
"context"
"errors"
"fmt"
"strconv"
"sync"
)
type Result struct {
Job int
Value int
}
func run(ctx context.Context, workers int, input []int) <-chan Result {
jobs := make(chan int, workers)
results := make(chan Result, workers)
go func() {
defer close(jobs)
for _, job := range input {
select {
case jobs <- job:
case <-ctx.Done():
return
}
}
}()
var wg sync.WaitGroup
for range workers {
wg.Go(func() {
for job := range jobs {
result := Result{Job: job, Value: job * job}
select {
case results <- result:
case <-ctx.Done():
return
}
}
})
}
go func() {
wg.Wait()
close(results)
}()
return results
}
func parse(args []string) ([]int, error) {
values := make([]int, 0, len(args))
for _, arg := range args {
value, err := strconv.Atoi(arg)
if err != nil {
return nil, fmt.Errorf("parse %q: %w", arg, err)
}
values = append(values, value)
}
if len(values) == 0 {
return nil, errors.New("provide at least one integer")
}
return values, nil
}
func main() {
values, err := parse([]string{"2", "3", "4", "5"})
if err != nil {
panic(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for result := range run(ctx, 3, values) {
fmt.Printf("%d -> %d\n", result.Job, result.Value)
}
}
若真实处理函数可能失败,可让 worker 返回 struct { Result Result; Err error },由拥有者记录首错并取消 context,再等待所有 worker 退出。不要在收到首错后直接放弃接收,而让其余 worker 阻塞在结果发送上。
24. 设计审查清单与结论
审查每个 channel 时,应能明确写出以下答案:
- channel 在哪里创建,容量依据是什么;
- 发送者有哪些,是否可能在接收者退出后继续发送;
- 谁是唯一关闭者,它如何证明所有发送已经结束;
- 消费者通过关闭、固定数量还是 context 判断结束;
- 每个阻塞点能否在取消或 deadline 后退出;
- 错误是否会取消同组任务,谁负责等待它们结束;
- 载荷是复制、不可变共享还是发生所有权转移;
- 过载时是阻塞、拒绝还是丢弃,是否有指标;
- 测试是否覆盖关闭、取消、零值、满队列和多生产者;
- 进程关闭时,队列是排空还是允许放弃。
channel 的可靠性最终来自完整、可证明的通信协议,而不是语法上的箭头。先确定生命周期和所有权,再选择无缓冲或有缓冲;先保证所有路径能结束,再讨论吞吐和微观性能。
系列导航与关联阅读
- 系列入口:Go 完整技术体系学习路线:从语法、并发到框架、中间件与 AI
- 上一篇:Go goroutine 与调度器:从 go 语句到 G-M-P 模型
- 下一篇:Go select、超时、Timer 与 Ticker:协调多个并发事件
- 延伸:Go 并发模式:Worker Pool、Pipeline、Fan-out 与背压
官方资料
本文依据 Go 官方规范、标准库文档和 Go 官方博客重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论